diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1f03f818c..890b9e1a7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -53,8 +53,6 @@ PODS: - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) - - music_service (0.0.1): - - Flutter - ota (0.0.1): - Flutter - permission_handler_apple (9.3.0): @@ -96,7 +94,6 @@ DEPENDENCIES: - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) - fluwx (from `.symlinks/plugins/fluwx/ios`) - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) - - music_service (from `.symlinks/plugins/music_service/ios`) - ota (from `.symlinks/plugins/ota/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`) @@ -143,8 +140,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluwx/ios" haptic_feedback: :path: ".symlinks/plugins/haptic_feedback/ios" - music_service: - :path: ".symlinks/plugins/music_service/ios" ota: :path: ".symlinks/plugins/ota/ios" permission_handler_apple: @@ -175,7 +170,6 @@ SPEC CHECKSUMS: haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d ota: 20c3e522c57e7ba70ce118109e73650acc9fec76 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6417d441b..bad551a70 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -252,7 +252,8 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -792,12 +793,23 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ +/* Begin XCRemoteSwiftPackageReference section */ + D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/flutterfire"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index eeb4e2248..93960f0d8 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -418,39 +418,14 @@ class AgentController extends GetxController { break; case AgentServiceEventType.functionCallResult: - final responseId = event.data['responseId'] ?? ''; - final result = event.data['result']; - final functionCall = event.data['function_call'] ?? ''; - Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall'); - if (result != null) { - // result 可能已经是 Map 类型,也可能是 String 类型 - Map parsedResult; - if (result is String && result.isNotEmpty) { - try { - parsedResult = jsonDecode(result); - } catch (e) { - Logger.e(TAG, '解析result数据失败: $e'); - break; - } - } else if (result is Map) { - parsedResult = result; - } else { - Logger.e(TAG, 'result数据类型不支持: ${result.runtimeType}'); - break; - } - - final meta = parsedResult["meta"]; + final responseId = event.data['responseId'] ?? ''; + // final result = event.data['result']; + final metaStr = event.data['meta'] ?? ''; + // final functionCall = event.data['function_call'] ?? ''; + Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr'); + if (metaStr.isNotEmpty) { Map metaResult = {}; - - if (meta != null && meta is String && meta.isNotEmpty) { - try { - metaResult = jsonDecode(meta) as Map; - } catch (e) { - Logger.e(TAG, '解析meta数据失败: $e'); - } - } else if (meta != null && meta is Map) { - metaResult = meta; - } + metaResult = jsonDecode(metaStr) as Map; if (metaResult.isNotEmpty) { // 判断是否为新的回复或响应ID是否改变 if (_isNewAssistantResponse || _currentResponseId != responseId) { @@ -471,9 +446,12 @@ class AgentController extends GetxController { ); } } - if (metaResult.containsKey("card_navigation")) { - final start = metaResult["card_navigation"]["start"] as String; - final end = metaResult["card_navigation"]["end"] as String; + if (metaResult.containsKey("maps_route_navigation")) { + //导航 + final start = metaResult["maps_route_navigation"] + ["card_navigation"]["start"] as String; + final end = metaResult["maps_route_navigation"] + ["card_navigation"]["end"] as String; // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); Future.delayed(Duration(seconds: 2), () { // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 1035bc83d..ee6af73c5 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -230,17 +230,21 @@ class MessageBubble extends GetView { // 判断消息内容,显示特殊卡片 if (!isUser && meta != null) { // 判断消息类型,显示不同的卡片 - if (meta!.keys.contains('card_music')) { - final musicId = meta!['card_music']['id'].toString(); - final musicName = meta!['card_music']['name'] as String; - final musicUrl = meta!['card_music']['url'] as String; - final musicSinger = meta!['card_music']['sgener'] as String; - final musicImage = meta!['card_music']['image'] as String; + if (meta!.keys.contains('music_play')) { + final card_music = + meta!["music_play"]['card_music'] as Map; + final musicId = card_music!['id'].toString(); + final musicName = card_music!['name'] as String; + final musicUrl = card_music!['url'] as String; + final musicSinger = card_music!['sgener'] as String; + final musicImage = card_music!['image'] as String; //音乐卡片 return buildMusicItem( context, musicId, musicName, musicUrl, musicSinger, musicImage); - } else if (meta!.keys.contains('card_musiclist')) { - final rawList = meta!['card_musiclist']['musics'] as List; + } else if (meta!.keys.contains('music_playlist')) { + final card_musiclist = + meta!["music_playlist"]['card_musiclist'] as Map; + final rawList = card_musiclist['musics'] as List; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 @@ -264,13 +268,15 @@ class MessageBubble extends GetView { rawList.map((e) => Map.from(e)).toList(); // 旅游 return buildTravelContent(context, travellist); - } else if (meta!.keys.contains('card_navigation')) { - final start = meta!['card_navigation']['start'] as String; - final end = meta!['card_navigation']['end'] as String; - final destination = meta!['card_navigation']['destination'] as String; - final rawList = meta!['card_navigation']['items'] as List; + } else if (meta!.keys.contains('maps_route_navigation')) { + final card_navigation = meta!['maps_route_navigation'] + ['card_navigation'] as Map; + final start = card_navigation['start'] as String; + final end = card_navigation['end'] as String; + final destination = card_navigation['destination'] as String; + final rawList = card_navigation['items'] as List; final list = rawList.map((e) => Map.from(e)).toList(); - final rawNavigation = meta!['card_navigation']['navigation']; + final rawNavigation = card_navigation['navigation']; final Map? navigation = rawNavigation is Map ? rawNavigation : null; print("导航实时数据 ${rawNavigation}"); diff --git a/local_plugins/agent_service/ios/agent_service/Package.swift b/local_plugins/agent_service/ios/agent_service/Package.swift index 3cbf57b59..a4cf49980 100644 --- a/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/local_plugins/agent_service/ios/agent_service/Package.swift @@ -13,7 +13,8 @@ let package = Package( .package(path: "../../../speech/ios/speech"), .package(path: "../../../azure_speech/ios/azure_speech"), .package(path: "../../../chat_api/ios/chat_api"), - .package(path: "../../../chat_storage/ios/chat_storage") + .package(path: "../../../chat_storage/ios/chat_storage"), + .package(path: "../../../music_service/ios/music_service") ], targets: [ .target( @@ -24,7 +25,8 @@ let package = Package( .product(name: "speech", package: "speech"), .product(name: "azure-speech", package: "azure_speech"), .product(name: "chat-api", package: "chat_api"), - .product(name: "chat-storage", package: "chat_storage") + .product(name: "chat-storage", package: "chat_storage"), + .product(name: "music-service", package: "music_service") ], path: "Sources/agent_service", resources: [ diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 191c6968b..9f4ac434a 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -7,6 +7,7 @@ import os import os.log import chat_api import chat_storage +import music_service protocol AgentServiceListener: AnyObject { func onEvent(eventName: String, data: [String: Any]) @@ -595,43 +596,98 @@ class AgentServiceImpl: NSObject { } } } - + + //解析卡片数据 @discardableResult - internal func autoHandleFunctionCallResult(_ functionCallResult: [String: Any]) -> String { - guard let metaStr = functionCallResult["meta"] as? String, !metaStr.isEmpty else { - return "" - } - - do { - guard let metaData = metaStr.data(using: .utf8), - let meta = try JSONSerialization.jsonObject(with: metaData) as? [String: Any] else { - return metaStr - } - - if let cardMusic = meta["card_music"] as? [String: Any] { - let id = cardMusic["id"] as? String ?? "" - let url = cardMusic["url"] as? String ?? "" - let name = cardMusic["name"] as? String ?? "" - let sgener = cardMusic["sgener"] as? String ?? "" - let image = cardMusic["image"] as? String ?? "" - - processMusicPlay([ - "id": id, - "url": url, - "title": name, - "artist": sgener, - "coverUrl": image - ]) + internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: [String: Any]) -> (String, Bool) { + if let contentArray = functionCallResult["content"] as? [[String: Any]], + let firstContent = contentArray.first, + let textStr = firstContent["text"] as? String, + let jsonData = textStr.data(using: .utf8) { + do { + if let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { + print("解析成功:\(jsonObject)") + var metadata: [String: Any] = [:] + metadata[toolName] = jsonObject + // 将 metadata 转为 JSON 字符串 + let metaStr: String + if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: [.prettyPrinted]), + let jsonString = String(data: jsonData, encoding: .utf8) { + metaStr = jsonString + } else { + metaStr = "" + } + // 处理 card_music + if let cardMusic = jsonObject["card_music"] as? [String: Any] { + let id = cardMusic["id"] as? String ?? "" + let url = cardMusic["url"] as? String ?? "" + let title = cardMusic["name"] as? String ?? "" + let artist = cardMusic["sgener"] as? String ?? "" + let coverUrl = cardMusic["image"] as? String ?? "" + + processMusicPlay([ + "id": id, + "url": url, + "title": title, + "artist": artist, + "coverUrl": coverUrl + ]) + return (metaStr, false) + // 处理 card_musiclist + } else if let cardMusiclist = jsonObject["card_musiclist"] as? [String: Any], + let musics = cardMusiclist["musics"] as? [[String: Any]] { + + var playlist: [[String: String]] = [] + + for item in musics { + let id = item["id"] as? String ?? "" + let url = item["url"] as? String ?? "" + let title = item["name"] as? String ?? "" + let artist = item["sgener"] as? String ?? "" + let coverUrl = item["image"] as? String ?? "" + + let song: [String: String] = [ + "id": id, + "url": url, + "title": title, + "artist": artist, + "coverUrl": coverUrl + ] + playlist.append(song) + } + + if !playlist.isEmpty { + print("自动播放音乐列表: \(playlist)") + processMusicPlayList(playlist) + return (metaStr, false) + } else { + print("card_musiclist 中没有有效的音乐条目") + } + }else if let cardNavigation = jsonObject["card_navigation"] as? [String: Any] { //导航 + let start:String = cardNavigation["start"] as? String ?? "" + let end:String = cardNavigation["end"] as? String ?? "" + // processNavigation(start,end) + return (metaStr, false) + } + return (metaStr, true) + } + } catch { + print("JSON 解析失败: \(error)") } - - return metaStr - } catch { - return metaStr } + return ("", true) } - private func processMusicPlay(_ musicData: [String: Any]) { - sendEvent(name: "music_play", data: musicData) + // 后台播放单曲 + private func processMusicPlay(_ song: [String: String]) { + sendEvent(name: "music_play", data: song) + MusicService.shared.play(song: song) + } + + // 后台播放音乐列表 + private func processMusicPlayList(_ songs: [[String: String]]) { + // sendEvent(name: "music_playlist", data: songs) + MusicService.shared.playPlaylist(songs: songs) } func clearChatHistory() -> Bool { @@ -694,6 +750,7 @@ class ChatApiStreamCallback: StreamCallback { private let hasImage: Bool private var responseBuilder = "" private var metadata = "" + private var nobroadcast = true init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { self.agentService = agentService @@ -712,7 +769,7 @@ class ChatApiStreamCallback: StreamCallback { agentService.sendEvent(name: "assistant_token", data: ["token": token]) - if speakResponse { + if speakResponse && nobroadcast{ agentService.azureTtsHelper?.speakStream(token) } } @@ -721,7 +778,7 @@ class ChatApiStreamCallback: StreamCallback { guard let agentService = agentService else { return } - if speakResponse { + if speakResponse && nobroadcast{ agentService.azureTtsHelper?.flushStream() } @@ -792,16 +849,18 @@ class ChatApiStreamCallback: StreamCallback { guard let agentService = agentService else { return } let name = functionCall["name"] as? String ?? "" - os_log("收到函数调用结果: %{public}@", log: agentService.logger, type: .info, name) + os_log("收到函数调用结果: %{public}@", log: agentService.logger, type: .info, functionCallResult) agentService.audioPlayer?.stopCallingSound() - + let (metestr, broadcast) = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) + metadata = metestr; + nobroadcast = broadcast + os_log("卡片数据: %{public}@", log: agentService.logger, type: .info, metadata) agentService.sendEvent(name: "function_call_result", data: [ "function_call": functionCall, - "result": functionCallResult + "result": functionCallResult, + "meta":metadata, ]) - - metadata = agentService.autoHandleFunctionCallResult(functionCallResult) os_log("处理函数调用结果完成", log: agentService.logger, type: .info) } } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index ad1f9ef2b..c5638966d 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -715,15 +715,26 @@ public class ChatApiService: NSObject { type == "function", let function = tool["function"] as? [String: Any], let name = function["name"] as? String, - let description = function["description"] as? String else { + let description = function["description"] as? String, + let parameters: [String: Any] = function["parameters"] as? [String: Any] else { + continue + } + // 将字典转换为 JSON 数据 + guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { + print("无法将 parameters 转换为 JSON 数据") + continue + } + // 解码 JSON 数据为 AnyJSONSchema 类型 + let decoder = JSONDecoder() + guard let schema = try? decoder.decode(AnyJSONSchema.self, from: jsonData) else { + print("无法将 JSON 数据解码为 AnyJSONSchema") continue } - // 创建函数定义 let functionDefinition = ChatQuery.ChatCompletionToolParam.FunctionDefinition( name: name, description: description, - parameters: nil + parameters: schema ) // 创建工具参数 diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index 11f0a7266..aba509fc5 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -31,16 +31,18 @@ public protocol FunctionHandler { public class MCPSubClient { private let serverId: String private let serverUrl: String - + private let filterTools: String + // 使用官方MCP Swift SDK private var mcpClient: Client? private var tools: [Tool] = [] private var toolMaps: [[String: Any]] = [] private var isConnectedFlag = false - public init(serverId: String, serverUrl: String) { + public init(serverId: String, serverUrl: String,filterTools:String) { self.serverId = serverId self.serverUrl = serverUrl + self.filterTools = filterTools } public func connect() async -> Bool { @@ -78,6 +80,9 @@ public class MCPSubClient { toolMaps.removeAll() for tool in toolList { + if !filterTools.isEmpty && !filterTools.contains(tool.name) { + continue + } let parameters = convertInputSchema(tool.inputSchema) let functionMap: [String: Any] = [ @@ -271,8 +276,8 @@ public class MCPClient { guard let url = configDict["url"] as? String, !url.isEmpty else { continue } - - let subClient = MCPSubClient(serverId: serverId, serverUrl: url) + let filtertools = configDict["tools"] as? String ?? "" + let subClient = MCPSubClient(serverId: serverId, serverUrl: url,filterTools: filtertools) if await subClient.connect() { subClients[serverId] = subClient diff --git a/local_plugins/music_service/ios/music_service/Package.swift b/local_plugins/music_service/ios/music_service/Package.swift new file mode 100644 index 000000000..f8268662a --- /dev/null +++ b/local_plugins/music_service/ios/music_service/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "music_service", + platforms: [.iOS("18.0")], + products: [ + .library(name: "music-service", targets: ["music_service"]) + ], + dependencies: [ + // 例如,添加 Alamofire 作为网络请求库 + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0") + ], + targets: [ + .target( + name: "music_service", + dependencies: [ + // 指定使用 Alamofire 库 + .product(name: "Alamofire", package: "Alamofire") + ], + path: "Sources/music_service" + ) + ] +) \ No newline at end of file diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift new file mode 100644 index 000000000..92c4e9a21 --- /dev/null +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -0,0 +1,120 @@ +// +// MusicService.swift +// music_service +// +// Created by 李伟 on 2025/6/4. +// +import Foundation +import AVFoundation +import MediaPlayer +import UIKit + +public class MusicService: NSObject { + public static let shared: MusicService = MusicService() + + private var player: AVPlayer? + private var playlist: [[String: String]] = [] + private var currentIndex = 0 + public var isPlaying = false + + private override init() { + super.init() + setupRemoteCommandCenter() + } + + func getCurrentUrlString() -> String? { + guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else { + return nil + } + return urlAsset.url.absoluteString + } + + public func play(song: [String: String]) { + guard let urlString = song["url"], let url = URL(string: urlString) else { return } + player = AVPlayer(url: url) + player?.play() + isPlaying = true + updateNowPlayingInfo(song: song) + } + + public func playPlaylist(songs: [[String: String]]) { + playlist = songs + currentIndex = 0 + play(song: playlist[currentIndex]) + } + + func pause() { + player?.pause() + isPlaying = false + updatePlaybackState() + } + + func resume() { + player?.play() + isPlaying = true + updatePlaybackState() + } + + func next() { + guard currentIndex + 1 < playlist.count else { return } + currentIndex += 1 + play(song: playlist[currentIndex]) + } + + func previous() { + guard currentIndex - 1 >= 0 else { return } + currentIndex -= 1 + play(song: playlist[currentIndex]) + } + func stop() { + player?.pause() + player?.replaceCurrentItem(with: nil) + isPlaying = false + updatePlaybackState() + } + func setVolume(volume: Float) { + player?.volume = volume + } + private func setupRemoteCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + + commandCenter.playCommand.addTarget { [unowned self] _ in + self.resume() + return .success + } + + commandCenter.pauseCommand.addTarget { [unowned self] _ in + self.pause() + return .success + } + + commandCenter.nextTrackCommand.addTarget { [unowned self] _ in + self.next() + return .success + } + + commandCenter.previousTrackCommand.addTarget { [unowned self] _ in + self.previous() + return .success + } + } + + private func updateNowPlayingInfo(song: [String: String]) { + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: song["title"] ?? "Unknown Title", + MPMediaItemPropertyArtist: song["artist"] ?? "Unknown Artist" + ] + + if let coverUrlString = song["coverUrl"], let coverUrl = URL(string: coverUrlString), + let data = try? Data(contentsOf: coverUrl), + let image = UIImage(data: data) { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + + private func updatePlaybackState() { + // iOS manages playback state automatically, but you can update NowPlayingInfo if needed + } +} diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift new file mode 100644 index 000000000..a81039fa6 --- /dev/null +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift @@ -0,0 +1,71 @@ +import Flutter +import UIKit + +public class MusicServicePlugin: NSObject, FlutterPlugin { + private static var channel: FlutterMethodChannel? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.music_service", binaryMessenger: registrar.messenger()) + let instance = MusicServicePlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + self.channel = channel + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + print("Method call: \(call.method)") + + switch call.method { + case "play": + if let args = call.arguments as? [String: Any], + let song = args["song"] as? [String: String] { + MusicService.shared.play(song: song) + result(nil) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing 'song' map", details: nil)) + } + + case "playlist": + if let args = call.arguments as? [String: Any], + let songs = args["songs"] as? [[String: String]], !songs.isEmpty { +// MusicServiceStarter.startServiceWithPlaylist(command: "playlist", songs: songs) + MusicService.shared.playPlaylist(songs: songs) + result(nil) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or empty 'songs' list", details: nil)) + } + + case "pause": + MusicService.shared.pause() + result(nil) + + case "resume": + MusicService.shared.resume() + result(nil) + + case "stop": +// MusicServiceStarter.startService(command: "stop") + MusicService.shared.stop() + result(nil) + + case "setVolume": + if let args = call.arguments as? [String: Any], + let volume = args["volume"] as? Double { + MusicService.shared.setVolume(volume: Float(volume)) + result(nil) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or invalid 'volume'", details: nil)) + } + + case "getStatus": + let isPlaying = MusicService.shared.isPlaying + let currentUrl = MusicService.shared.getCurrentUrlString() + let status: [String: Any] = [ + "isPlaying": isPlaying, + "url": currentUrl != nil ? currentUrl! : NSNull() + ] + result(status) + default: + result(FlutterMethodNotImplemented) + } + } +}