From 5e2278ffe776f934024d1a20b1738bd1baf14980 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 16 Jul 2025 20:30:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=81=8A=E5=A4=A9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=92=8C=E9=9F=B3=E4=B9=90=E6=9C=8D=E5=8A=A1=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentServiceImpl.swift | 272 +++++++----- .../Sources/chat_api/ChatApiPlugin.swift | 145 ++----- .../Sources/chat_api/ChatApiService.swift | 112 +++-- .../Sources/music_service/MusicService.swift | 404 ++++++++++++++++-- .../music_service/MusicServicePlugin.swift | 37 +- 5 files changed, 663 insertions(+), 307 deletions(-) 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 78e299cf2..6209895f3 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 @@ -37,6 +37,7 @@ class AgentServiceImpl: NSObject { private var apiKey: String = "" private var baseUrl: String = "https://api.openai.com/v1/chat/completions" private var model: String = "gpt-3.5-turbo" + private var visionModel = "gpt-4-vision-preview" private var systemPrompt: String = "" private var chatHistory: [[String: Any]] = [] @@ -55,6 +56,8 @@ class AgentServiceImpl: NSObject { private var hasSpeechDetected: Bool = false internal var isAiStreaming: Bool = false + var currsessionId: String = "" + private var idleTimer: DispatchSourceTimer? private let maxIdleSeconds: TimeInterval = 10 @@ -245,6 +248,7 @@ class AgentServiceImpl: NSObject { apiKey: apiKey, baseUrl: baseUrl, model: model, + visionModel:visionModel, mcpServer: mcpServer ) @@ -482,10 +486,10 @@ audioStream.saveAudioDataTo(data: audioData) displayText: displayText, hasImage: hasImage ) - + currsessionId = UUID().uuidString os_log("设置回调并调用sendMessageStream", log: logger, type: .info) - chatApiService.setStreamCallback(callback) - chatApiService.sendMessageStream(messages: messages) + chatApiService.setStreamCallback(currsessionId,callback) + chatApiService.sendMessageStream(currsessionId,messages: messages) os_log("sendMessageStream调用完成", log: logger, type: .info) } @@ -592,27 +596,28 @@ audioStream.saveAudioDataTo(data: audioData) /** * 保存聊天消息 */ - internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "") { + internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { DispatchQueue.global(qos: .utility).async { // 保存用户消息 let userMessageId = self.chatStorageHelper.saveMessage( sessionId: self.sessionId, message: userMessage, sender: "user", - metadata: nil as String? + metadata: userMetadata ) if userMessageId != -1 { - // 保存AI回复 - let assistantMessageId = self.chatStorageHelper.saveMessage( - sessionId: self.sessionId, - message: assistantMessage, - sender: "assistant", - metadata: metadata.isEmpty ? nil : metadata - ) - - if assistantMessageId == -1 { - os_log("保存助手消息失败", log: self.logger, type: .error) + if !assistantMessage.isEmpty || !metadata.isEmpty { + // 保存AI回复 + let assistantMessageId = self.chatStorageHelper.saveMessage( + sessionId: self.sessionId, + message: assistantMessage, + sender: "assistant", + metadata: metadata.isEmpty ? nil : metadata + ) + if assistantMessageId == -1 { + os_log("保存助手消息失败", log: self.logger, type: .error) + } } } else { os_log("保存用户消息失败", log: self.logger, type: .error) @@ -622,16 +627,26 @@ audioStream.saveAudioDataTo(data: audioData) //解析卡片数据 @discardableResult - internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: [String: Any]) -> (String, Bool) { + internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: [String: Any]) -> [String:Any] { 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 { + var broadcast = true; + var notreply = false; if let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { print("解析成功:\(jsonObject)") var metadata: [String: Any] = [:] metadata[toolName] = jsonObject + + if let _broadcast = jsonObject["broadcast"] as? Bool { + broadcast = _broadcast + } + if let _notreply = jsonObject["notreply"] as? Bool { + notreply = _notreply + } + metadata["notreply"] = notreply // 将 metadata 转为 JSON 字符串 let metaStr: String if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: [.prettyPrinted]), @@ -642,7 +657,7 @@ audioStream.saveAudioDataTo(data: audioData) } // 处理 card_music if let cardMusic = jsonObject["card_music"] as? [String: Any] { - let id = cardMusic["id"] as? String ?? "" + let id = cardMusic["id"] as? Int ?? 0 let url = cardMusic["url"] as? String ?? "" let title = cardMusic["name"] as? String ?? "" let artist = cardMusic["sgener"] as? String ?? "" @@ -655,21 +670,21 @@ audioStream.saveAudioDataTo(data: audioData) "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]] = [] + var playlist: [[String: Any]] = [] for item in musics { - let id = item["id"] as? String ?? "" + let id = item["id"] as? Int ?? 0 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 ts = item["ts"] as? Int ?? 0 - let song: [String: String] = [ + let song: [String: Any] = [ "id": id, "url": url, "title": title, @@ -682,33 +697,38 @@ audioStream.saveAudioDataTo(data: audioData) 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) + return [ + "meta": metaStr, + "broadcast": broadcast, + "notreply": notreply + ] } } catch { print("JSON 解析失败: \(error)") } } - return ("", true) + return [ + "meta": "", + "broadcast": true, + "notreply": false, + ] } // 后台播放单曲 - private func processMusicPlay(_ song: [String: String]) { + private func processMusicPlay(_ song: [String: Any]) { sendEvent(name: "music_play", data: song) MusicService.shared.play(song: song) } // 后台播放音乐列表 - private func processMusicPlayList(_ songs: [[String: String]]) { + private func processMusicPlayList(_ songs: [[String: Any]]) { // sendEvent(name: "music_playlist", data: songs) MusicService.shared.playPlaylist(songs: songs) } @@ -795,7 +815,9 @@ class ChatApiStreamCallback: StreamCallback { private let hasImage: Bool private var responseBuilder = "" private var metadata = "" - private var nobroadcast = true + private var userMetadata:[String: Any] = [:] + private var reply = true; //是否回复 + private var broadcast = true //是否播发 init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { self.agentService = agentService @@ -805,31 +827,39 @@ class ChatApiStreamCallback: StreamCallback { } - - func onToken(_ token: String) { - guard let agentService = agentService else { return } + func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){ - - responseBuilder += token - - agentService.sendEvent(name: "assistant_token", data: ["token": token]) - - if speakResponse && nobroadcast{ - agentService.azureTtsHelper?.speakStream(token) + } + func onToken(_ sessionId:String,_ token: String) { + do { + guard let agentService = try agentService else { return } + if (sessionId != agentService.currsessionId) { + return + } + + responseBuilder += token + + if speakResponse && reply && broadcast{ + try agentService.azureTtsHelper?.speakStream(token) + } + if (reply && token != "") { + try agentService.sendEvent(name: "assistant_token", data: ["token": token]) + } + }catch { + print("liwei--------------- AI Call onToken 异常: \(error)") } } - func onComplete() { - guard let agentService = agentService else { return } - - - if speakResponse && nobroadcast{ - agentService.azureTtsHelper?.flushStream() - } - - let response = responseBuilder - - if !response.isEmpty { + func onComplete(_ sessionId:String) { + do { + guard let agentService = try agentService else { return } + + if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{ + agentService.azureTtsHelper?.flushStream() + } + + let response = responseBuilder + var responseData: [String: Any] = [ "text": response, "userInput": displayText @@ -837,77 +867,109 @@ class ChatApiStreamCallback: StreamCallback { if hasImage { responseData["hasImage"] = true } - agentService.sendEvent(name: "assistant_response", data: responseData) + if (reply) { + try agentService.sendEvent(name: "assistant_response", data: responseData) + } let assistantMessage: [String: Any] = [ "role": "assistant", "content": response ] - agentService.addToHistoryMessages(assistantMessage) - + if !response.isEmpty { + try agentService.addToHistoryMessages(assistantMessage) + } // 保存聊天记录 - agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata) + try agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata)) + agentService.isAiStreaming = false + os_log("设置AI流式状态为false", log: agentService.logger, type: .info) + }catch{ + print("liwei--------------- AI Call onComplete 异常: \(error)") + } + + } + + func onError(_ sessionId:String,_ error: Error) { + do { + guard let agentService = try agentService else { return } - os_log("保存聊天记录完成", log: agentService.logger, type: .info) - } else { - os_log("警告: 响应内容为空", log: agentService.logger, type: .error) + os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription) + if (sessionId != agentService.currsessionId) { + return + } + try agentService.sendEvent(name: "error", data: [ + "code": "AI_ERROR", + "message": error.localizedDescription + ]) + + agentService.isAiStreaming = false + os_log("因错误设置AI流式状态为false", log: agentService.logger, type: .info) + }catch{ + print("liwei--------------- AI Call onError 异常: \(error)") } - - agentService.isAiStreaming = false - os_log("设置AI流式状态为false", log: agentService.logger, type: .info) + } - func onError(_ error: Error) { - guard let agentService = agentService else { return } - - os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription) - - agentService.sendEvent(name: "error", data: [ - "code": "AI_ERROR", - "message": error.localizedDescription - ]) - - agentService.isAiStreaming = false - os_log("因错误设置AI流式状态为false", log: agentService.logger, type: .info) + func onFunctionCall(_ sessionId:String,_ functionCall: [String: Any]) { + do { + guard let agentService = try agentService else { return } + if (sessionId != agentService.currsessionId) { + return + } + let name = functionCall["name"] as? String ?? "" + os_log("收到函数调用: %{public}@", log: agentService.logger, type: .info, name) + + try agentService.audioPlayer?.playCallingSound() + + try agentService.sendEvent(name: "function_call", data: [ + "name": name, + "arguments": functionCall + ]) + + if name == "exit_interaction" { + os_log("执行退出交互", log: agentService.logger, type: .info) + agentService.stopRecognition() + } + + }catch{ + print("liwei--------------- AI Call onFunctionCall 异常: \(error)") + } + } - func onFunctionCall(_ functionCall: [String: Any]) { - guard let agentService = agentService else { return } - - let name = functionCall["name"] as? String ?? "" - os_log("收到函数调用: %{public}@", log: agentService.logger, type: .info, name) - - agentService.audioPlayer?.playCallingSound() - - agentService.sendEvent(name: "function_call", data: [ - "name": name, - "arguments": functionCall - ]) - - if name == "exit_interaction" { - os_log("执行退出交互", log: agentService.logger, type: .info) - agentService.stopRecognition() + func onFunctionCallResult(_ sessionId:String,_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { + do { + guard let agentService = agentService else { return } + + let name = functionCall["name"] as? String ?? "" + os_log("收到函数调用结果: %{public}@", log: agentService.logger, type: .info, functionCallResult) + + agentService.audioPlayer?.stopCallingSound() + let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) + metadata = resultmap["meta"] as? String ?? "" + broadcast = resultmap["broadcast"] as? Bool ?? true + reply = !(resultmap["notreply"] as? Bool ?? true) + os_log("卡片数据: %{public}@", log: agentService.logger, type: .info, metadata) + agentService.sendEvent(name: "function_call_result", data: [ + "function_call": functionCall, + "result": functionCallResult, + "meta":metadata, + ]) + os_log("处理函数调用结果完成", log: agentService.logger, type: .info) + }catch{ + print("liwei--------------- AI Call onFunctionCallResult 异常: \(error)") } + } - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { - guard let agentService = agentService else { return } - - let name = functionCall["name"] as? String ?? "" - 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, - "meta":metadata, - ]) - os_log("处理函数调用结果完成", log: agentService.logger, type: .info) + func toJSONString(_ dict: [String: Any]) -> String { + guard JSONSerialization.isValidJSONObject(dict), + let data = try? JSONSerialization.data(withJSONObject: dict), + let json = String(data: data, encoding: .utf8) else { + return "" + } + return json } + } extension AgentServiceImpl: TtsEventListener { diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift index ee4153b65..5bb2c7eab 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift @@ -18,25 +18,25 @@ public class ChatApiPlugin: NSObject, FlutterPlugin { let instance = ChatApiPlugin() registrar.addMethodCallDelegate(instance, channel: methodChannel) - eventChannel.setStreamHandler(instance) +// eventChannel.setStreamHandler(instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "initialize": handleInitialize(call: call, result: result) - case "createUserMessage": - handleCreateUserMessage(call: call, result: result) - case "createAssistantMessage": - handleCreateAssistantMessage(call: call, result: result) - case "sendMessage": - handleSendMessage(call: call, result: result) - case "sendMessageStream": - handleSendMessageStream(call: call, result: result) - case "sendFunctionCallResult": - handleSendFunctionCallResult(call: call, result: result) - case "cancelCurrentStream": - handleCancelCurrentStream(result: result) +// case "createUserMessage": +// handleCreateUserMessage(call: call, result: result) +// case "createAssistantMessage": +// handleCreateAssistantMessage(call: call, result: result) +// case "sendMessage": +// handleSendMessage(call: call, result: result) +// case "sendMessageStream": +// handleSendMessageStream(call: call, result: result) +// case "sendFunctionCallResult": +// handleSendFunctionCallResult(call: call, result: result) +// case "cancelCurrentStream": +// handleCancelCurrentStream(result: result) case "registerFunction": handleRegisterFunction(call: call, result: result) case "initializeMcpClient": @@ -63,14 +63,16 @@ public class ChatApiPlugin: NSObject, FlutterPlugin { let baseUrl = args["baseUrl"] as? String ?? "" let model = args["model"] as? String ?? "" + let visionModel = args["modelmedia"] as? String ?? "" let mcpServer = args["mcpServer"] as? String ?? "" - service.setStreamCallback(StreamCallbackImpl(eventSink: eventSink)) +// service.setStreamCallback(StreamCallbackImpl(eventSink: eventSink)) let success = service.initialize( apiKey: apiKey, baseUrl: baseUrl, model: model, + visionModel:visionModel, mcpServer: mcpServer ) @@ -120,16 +122,16 @@ public class ChatApiPlugin: NSObject, FlutterPlugin { } } - private func handleSendMessageStream(call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any], - let messages = args["messages"] as? [[String: Any]] else { - result(FlutterError(code: "INVALID_ARGS", message: "缺少必要参数", details: nil)) - return - } - - service.sendMessageStream(messages: messages) - result(true) - } +// private func handleSendMessageStream(call: FlutterMethodCall, result: @escaping FlutterResult) { +// guard let args = call.arguments as? [String: Any], +// let messages = args["messages"] as? [[String: Any]] else { +// result(FlutterError(code: "INVALID_ARGS", message: "缺少必要参数", details: nil)) +// return +// } +// +// service.sendMessageStream(messages: messages) +// result(true) +// } private func handleSendFunctionCallResult(call: FlutterMethodCall, result: @escaping FlutterResult) { result(FlutterError(code: "DEPRECATED", message: "sendFunctionCallResult已废弃,工具调用结果现在自动处理", details: nil)) @@ -199,98 +201,3 @@ public class ChatApiPlugin: NSObject, FlutterPlugin { } } -// MARK: - FlutterStreamHandler -extension ChatApiPlugin: FlutterStreamHandler { - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - if let callback = service.streamCallback as? StreamCallbackImpl { - callback.eventSink = events - } - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - self.eventSink = nil - if let callback = service.streamCallback as? StreamCallbackImpl { - callback.eventSink = nil - } - return nil - } -} - -// MARK: - StreamCallback Implementation -class StreamCallbackImpl: StreamCallback { - var eventSink: FlutterEventSink? - - init(eventSink: FlutterEventSink?) { - self.eventSink = eventSink - } - - func onToken(_ token: String) { - DispatchQueue.main.async { - self.eventSink?([ - "type": "token", - "content": token - ]) - } - } - - func onComplete() { - DispatchQueue.main.async { - self.eventSink?([ - "type": "complete" - ]) - } - } - - func onError(_ error: Error) { - DispatchQueue.main.async { - self.eventSink?([ - "type": "error", - "content": error.localizedDescription - ]) - } - } - - func onFunctionCall(_ functionCall: [String: Any]) { - DispatchQueue.main.async { - do { - let jsonData = try JSONSerialization.data(withJSONObject: functionCall) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" - self.eventSink?([ - "type": "functionCall", - "content": jsonString - ]) - } catch { - self.eventSink?([ - "type": "error", - "content": "Failed to serialize function call" - ]) - } - } - } - - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { - DispatchQueue.main.async { - do { - let meta = [ - "functionCall": functionCall, - "functionCallResult": functionCallResult - ] - let metaData = try JSONSerialization.data(withJSONObject: meta) - let metaString = String(data: metaData, encoding: .utf8) ?? "{}" - - self.eventSink?([ - "type": "functionCall", - "content": functionCallResult["context"] as? String ?? "", - "meta": metaString - ]) - } catch { - self.eventSink?([ - "type": "error", - "content": "Failed to serialize function call result" - ]) - } - } - } -} 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 cf1c6a3a0..a63e6a74a 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 @@ -15,13 +15,14 @@ public struct ChatApiException: Error { } } -/// 流式回调协议 +///// 流式回调协议 public protocol StreamCallback { - func onToken(_ token: String) - func onComplete() - func onError(_ error: Error) - func onFunctionCall(_ functionCall: [String: Any]) - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) + func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?) + func onToken(_ sessionId:String,_ token: String) + func onComplete(_ sessionId:String) + func onError(_ sessionId:String,_ error: Error) + func onFunctionCall(_ sessionId:String,_ functionCall: [String: Any]) + func onFunctionCallResult(_ sessionId:String,_ functionCall: [String: Any], _ functionCallResult: [String: Any]) } /// 工具调用信息 @@ -54,11 +55,10 @@ public class ChatApiService: NSObject { // 流式请求相关 private var currentStreamTask: Task? - var streamCallback: StreamCallback? + private var sessionCallbacks: [String: StreamCallback] = [:] private var currentMessages: [[String: Any]] = [] private var toolCalls: [Int: ToolCallInfo] = [:] - private var isCanceled = false - + private var currSessionId = "" // MARK: - 初始化 public override init() { super.init() @@ -67,12 +67,16 @@ public class ChatApiService: NSObject { // MARK: - 公共方法 /// 设置流式回调 - public func setStreamCallback(_ callback: StreamCallback) { - self.streamCallback = callback + public func setStreamCallback(_ sessionId:String,_ callback: StreamCallback) { + self.sessionCallbacks[sessionId] = callback + } + + public func getSessionCallback(_ sessionId:String)->StreamCallback?{ + return self.sessionCallbacks[sessionId] } /// 初始化ChatAPI服务 - public func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) -> Bool { + public func initialize(apiKey: String, baseUrl: String, model: String, visionModel:String, mcpServer: String) -> Bool { self.apiKey = apiKey if !baseUrl.isEmpty { self.baseUrl = baseUrl @@ -80,7 +84,9 @@ public class ChatApiService: NSObject { if !model.isEmpty { self.model = model } - + if !visionModel.isEmpty { + self.visionModel = visionModel + } // 分段解析baseUrl为OpenAI库需要的格式 guard let url = URL(string: self.baseUrl) else { return false @@ -246,15 +252,21 @@ public class ChatApiService: NSObject { } /// 发送消息(流式输出) - public func sendMessageStream(messages: [[String: Any]]) { + public func sendMessageStream(_ sessionId:String,messages: [[String: Any]]) { + // 1. 立即中止旧会话 + abortCurrentSession() + + // 2. 创建新会话 + currSessionId = sessionId + guard isInitialized && !apiKey.isEmpty, let openAI = openAI else { let error = ChatApiException("ChatAPI服务未初始化") - streamCallback?.onError(error) + getSessionCallback(sessionId)?.onError(sessionId,error) return } + // 重置状态 - isCanceled = false currentMessages = messages toolCalls = [:] @@ -298,8 +310,8 @@ public class ChatApiService: NSObject { let chatsStream: AsyncThrowingStream = openAI.chatsStream(query: query) for try await result in chatsStream { - if self.isCanceled { - break + if sessionId != self.currSessionId { + return } if let choice = result.choices.first { @@ -307,7 +319,7 @@ public class ChatApiService: NSObject { // 处理普通文本内容 if let content = delta.content { - self.streamCallback?.onToken(content) + getSessionCallback(sessionId)?.onToken(sessionId,content) } // 收集工具调用信息 @@ -342,25 +354,25 @@ public class ChatApiService: NSObject { } } - if !self.isCanceled { + if sessionId == self.currSessionId { // 处理工具调用或完成 - let hasToolCalls = await self.processToolCalls() + let hasToolCalls = await self.processToolCalls(sessionId) if !hasToolCalls { - self.streamCallback?.onComplete() + getSessionCallback(sessionId)?.onComplete(sessionId) } } } catch { - if !self.isCanceled { + if sessionId == self.currSessionId { let chatApiError = ChatApiException("流式请求失败: \(error.localizedDescription)") - self.streamCallback?.onError(chatApiError) + getSessionCallback(sessionId)?.onError(sessionId,chatApiError) } } } } /// 处理工具调用 - private func processToolCalls() async -> Bool { + private func processToolCalls(_ sessionId:String) async -> Bool { guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid() else { return false } @@ -373,12 +385,12 @@ public class ChatApiService: NSObject { ] // 通知上层工具调用事件 - streamCallback?.onFunctionCall(functionCall) + getSessionCallback(sessionId)?.onFunctionCall(sessionId,functionCall) // 在后台队列处理工具调用 Task { do { - if !self.isCanceled { + if sessionId == self.currSessionId { // 调用工具 let result: [String: Any] let args = try self.parseJsonArguments(firstToolCall.arguments) @@ -388,9 +400,9 @@ public class ChatApiService: NSObject { result = ["context": "MCP客户端未初始化"] } - if !self.isCanceled { + if sessionId == self.currSessionId { // 处理结果 - self.streamCallback?.onFunctionCallResult(functionCall, result) + getSessionCallback(sessionId)?.onFunctionCallResult(sessionId,functionCall, result) // 将result转换为JSON字符串 let context: String @@ -403,6 +415,7 @@ public class ChatApiService: NSObject { // 将结果发送回OpenAI继续对话 await self.sendFunctionCallResultInternal( + sessionId:sessionId, messages: self.currentMessages, functionCall: functionCall, functionResult: context @@ -410,9 +423,10 @@ public class ChatApiService: NSObject { } } } catch { - if !self.isCanceled { + if sessionId == self.currSessionId { let errorMessage = "工具调用处理失败: \(error.localizedDescription)" await self.sendFunctionCallResultInternal( + sessionId:sessionId, messages: self.currentMessages, functionCall: functionCall, functionResult: errorMessage @@ -426,11 +440,12 @@ public class ChatApiService: NSObject { /// 发送函数调用结果(内部方法) private func sendFunctionCallResultInternal( + sessionId:String, messages: [[String: Any]], functionCall: [String: Any], functionResult: String ) async { - if isCanceled { + if sessionId != self.currSessionId { return } @@ -459,12 +474,12 @@ public class ChatApiService: NSObject { ]) // 重新发起对话,让AI基于工具调用结果生成回复 - sendMessageStream(messages: fullMessages) + sendMessageStream(sessionId,messages: fullMessages) } /// 取消当前流式请求 public func cancelCurrentStream() -> Bool { - isCanceled = true + currSessionId = "" currentStreamTask?.cancel() currentStreamTask = nil return true @@ -543,6 +558,35 @@ public class ChatApiService: NSObject { return result?["context"] as? String ?? "无法处理MCP工具调用" } + + /// 中止当前会话 + private func abortCurrentSession() { + if !currSessionId.isEmpty { + let oldSessionId = currSessionId + + // 1. 取消当前的协程任务 + currentStreamTask?.cancel() + currentStreamTask = nil + + // 2. 通知旧会话被中止 + getSessionCallback(oldSessionId)?.onComplete(currSessionId) + + // 3. 清理状态 + currSessionId = "" + sessionCallbacks.removeValue(forKey: oldSessionId) + toolCalls.removeAll() + } + } + + /// 清理当前会话 + private func clearCurrentSession() { + let oldSessionId = currSessionId + currSessionId = "" + sessionCallbacks.removeValue(forKey: oldSessionId) + currentStreamTask = nil + toolCalls.removeAll() + } + // MARK: - 工具方法 /// 解析JSON参数 @@ -757,4 +801,4 @@ private class LocalFunctionHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { return "LOCAL_FUNCTION:\(functionName)" } -} \ 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 index fd0382cae..e9879a14b 100644 --- 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 @@ -14,71 +14,187 @@ public class MusicService: NSObject { public static let shared: MusicService = MusicService() private var player: AVPlayer? - private var playlist: [[String: String]] = [] + private var playlist: [[String: Any]] = [] private var currentIndex = 0 public var isPlaying = false private var flutterChannel: FlutterMethodChannel? private var progressTimer: Timer? + // 音量控制相关 + private var currentVolume: Float = 1.0 + private var originalVolume: Float = 1.0 + private var isVolumeReduced: Bool = false + + // 当前播放的歌曲ID + private var currentIdInternal: Int64? = nil + private override init() { super.init() setupRemoteCommandCenter() + setupAudioSession() + } + + // MARK: - 音频会话设置 + private func setupAudioSession() { + do { + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth]) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("设置音频会话失败: \(error)") + } } + // MARK: - 基础播放控制 func getCurrentUrlString() -> String? { guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else { return nil } return urlAsset.url.absoluteString } + + func getCurrentId() -> Int64? { + return currentIdInternal + } - 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 play(song: [String: Any]) { + guard let urlString = song["url"] as? String, let url = URL(string: urlString) else { return } + + // 获取歌曲ID + if let id = song["id"] as? NSNumber { + currentIdInternal = id.int64Value + } else { + currentIdInternal = 0 + } + + print("开始播放: \(urlString), ID: \(currentIdInternal ?? 0)") + + // 检查是否已在播放列表中 + let existingIndex = playlist.firstIndex { existingSong in + if let existingId = existingSong["id"] as? NSNumber, + let currentId = song["id"] as? NSNumber { + return existingId.int64Value == currentId.int64Value + } + return false + } + + if existingIndex == nil { + playlist.append(song) + currentIndex = playlist.count - 1 + } else { + currentIndex = existingIndex! + } + + // 创建播放项 + let playerItem = AVPlayerItem(url: url) + player = AVPlayer(playerItem: playerItem) + + // 设置播放器观察者 + setupPlayerObservers() + + // 监听播放项状态变化 + setupPlayerItemObservers(playerItem: playerItem, song: song) + + currentVolume = 1.0 + player?.volume = currentVolume + + print("播放器已创建,等待加载完成...") } - public func playPlaylist(songs: [[String: String]]) { + public func playPlaylist(songs: [[String: Any]]) { playlist = songs currentIndex = 0 - play(song: playlist[currentIndex]) + if !playlist.isEmpty { + play(song: playlist[currentIndex]) + } } func pause() { player?.pause() isPlaying = false updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: false) + stopProgressTimer() + print("已暂停") } func resume() { player?.play() isPlaying = true updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: true) + startProgressTimer() + print("继续播放") } func next() { - guard currentIndex + 1 < playlist.count else { return } - currentIndex += 1 - play(song: playlist[currentIndex]) + playNext() } func previous() { - guard currentIndex - 1 >= 0 else { return } - currentIndex -= 1 + playPrevious() + } + + private func playNext() { + guard !playlist.isEmpty else { return } + currentIndex = (currentIndex + 1) % playlist.count play(song: playlist[currentIndex]) } + + private func playPrevious() { + guard !playlist.isEmpty else { return } + currentIndex = currentIndex - 1 < 0 ? playlist.count - 1 : currentIndex - 1 + play(song: playlist[currentIndex]) + } + + private func playNextOrStop() { + if currentIndex + 1 < playlist.count { + currentIndex += 1 + play(song: playlist[currentIndex]) + } else { + stop() + } + } + func stop() { player?.pause() player?.replaceCurrentItem(with: nil) isPlaying = false + currentIdInternal = nil updatePlaybackState() + updateMediaSessionPlaybackState(isPlaying: false) + stopProgressTimer() + print("已停止") } + + // MARK: - 音量控制 func setVolume(volume: Float) { - player?.volume = volume + let clampedVolume = max(0.0, min(1.0, volume)) + currentVolume = clampedVolume + player?.volume = clampedVolume + print("设置音量: \(clampedVolume)") + } + + /// 降低音量(AI说话时) + func reduceVolume() { + if !isVolumeReduced { + originalVolume = currentVolume + let reducedVolume = max(0.0, min(1.0, currentVolume * 0.3)) // 降低到30% + setVolume(volume: reducedVolume) + isVolumeReduced = true + print("AI说话开始,音量从 \(originalVolume) 降低到 \(reducedVolume)") + } + } + + /// 恢复原始音量(AI说话结束时) + func restoreVolume() { + if isVolumeReduced { + setVolume(volume: originalVolume) + isVolumeReduced = false + print("AI说话结束,音量恢复到 \(originalVolume)") + } } + // MARK: - 进度控制 func getProgress() -> [String: Any] { guard let player = player, let currentItem = player.currentItem else { return [ @@ -87,27 +203,59 @@ public class MusicService: NSObject { "progress": 0.0 ] } - + let currentTime = CMTimeGetSeconds(player.currentTime()) let duration = CMTimeGetSeconds(currentItem.duration) - - // 处理无效值 - let currentPosition = currentTime.isNaN || currentTime.isInfinite ? 0 : Int(currentTime * 1000) - let totalDuration = duration.isNaN || duration.isInfinite ? 0 : Int(duration * 1000) - let progress = totalDuration > 0 ? currentTime / duration : 0.0 - - return [ + + // 更严格的有效性检查 + let currentPosition = (currentTime.isNaN || currentTime.isInfinite || currentTime < 0) ? 0 : Int(currentTime * 1000) + let totalDuration = (duration.isNaN || duration.isInfinite || duration <= 0) ? 0 : Int(duration * 1000) + let progress = (totalDuration > 0 && currentTime >= 0) ? currentTime / duration : 0.0 + + let result = [ "currentPosition": currentPosition, "duration": totalDuration, "progress": progress.isNaN || progress.isInfinite ? 0.0 : progress ] + + // 添加调试日志 + if totalDuration == 0 { + print("警告: 音频时长为0,可能还在加载中。原始duration: \(duration)") + } + + return result } func seekTo(positionMs: Int) { guard let player = player else { return } + + // 记录当前播放状态 + let wasPlaying = isPlaying + let time = CMTime(seconds: Double(positionMs) / 1000.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - player.seek(to: time) + + player.seek(to: time) { [weak self] completed in + if completed { + print("跳转到位置: \(positionMs)ms") + + // 如果之前是播放状态,确保继续播放 + if wasPlaying { + self?.player?.play() + self?.isPlaying = true + self?.updateMediaSessionPlaybackState(isPlaying: true) + self?.startProgressTimer() + } + + // 立即更新进度信息到系统媒体控制中心 + self?.updateProgressInfoImmediately() + + // 通知Flutter层 + self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) + } + } } + + // MARK: - 媒体会话控制 private func setupRemoteCommandCenter() { let commandCenter = MPRemoteCommandCenter.shared() @@ -130,21 +278,41 @@ public class MusicService: NSObject { self.previous() return .success } + + commandCenter.stopCommand.addTarget { [unowned self] _ in + self.stop() + return .success + } } - private func updateNowPlayingInfo(song: [String: String]) { + private func updateNowPlayingInfo(song: [String: Any]) { + let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手" + let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" + var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: song["title"] ?? "Unknown Title", - MPMediaItemPropertyArtist: song["artist"] ?? "Unknown Artist" + MPMediaItemPropertyTitle: title, + MPMediaItemPropertyArtist: 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 } + + // 异步加载封面图片 + if !coverUrl.isEmpty, let url = URL(string: coverUrl) { + DispatchQueue.global(qos: .background).async { + if let data = try? Data(contentsOf: url), + let image = UIImage(data: data) { + DispatchQueue.main.async { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } else { + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } + } + } else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } private func updatePlaybackState() { @@ -170,13 +338,21 @@ public class MusicService: NSObject { flutterChannel = channel setupEnhancedRemoteCommandCenter() startProgressTimer() + print("媒体会话回调已启用") } /// 更新媒体会话元数据 public func updateMediaSessionMetadata(song: [String: Any]) { - let title = song["title"] as? String ?? song["name"] as? String ?? "Unknown Title" - let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "Unknown Artist" + let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手" let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" + + // 更新当前歌曲ID + if let id = song["id"] as? NSNumber { + currentIdInternal = id.int64Value + } + + print("updateMediaSessionMetadata currentIdInternal: \(currentIdInternal ?? 0)") var nowPlayingInfo: [String: Any] = [ MPMediaItemPropertyTitle: title, @@ -201,6 +377,8 @@ public class MusicService: NSObject { } else { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } + + print("媒体会话元数据已更新: \(title) - \(artist)") } /// 更新媒体会话播放状态 @@ -215,6 +393,8 @@ public class MusicService: NSObject { } else { stopProgressTimer() } + + print("媒体会话播放状态已更新: \(isPlaying)") } /// 设置增强的远程控制中心 @@ -226,6 +406,7 @@ public class MusicService: NSObject { commandCenter.pauseCommand.removeTarget(nil) commandCenter.nextTrackCommand.removeTarget(nil) commandCenter.previousTrackCommand.removeTarget(nil) + commandCenter.changePlaybackPositionCommand.removeTarget(nil) // 播放命令 commandCenter.playCommand.addTarget { [weak self] _ in @@ -255,13 +436,12 @@ public class MusicService: NSObject { return .success } - // 跳转命令 + // 跳转命令 - 关键升级点 commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in if let event = event as? MPChangePlaybackPositionCommandEvent { let positionMs = Int(event.positionTime * 1000) self?.seekTo(positionMs: positionMs) - self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) return .success } return .commandFailed @@ -273,6 +453,8 @@ public class MusicService: NSObject { self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) return .success } + + print("增强的远程控制中心已设置,支持进度条滑动") } /// 启动进度定时器 @@ -297,11 +479,147 @@ public class MusicService: NSObject { let duration = CMTimeGetSeconds(currentItem.duration) if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { - var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + // 更新系统媒体信息 - 关键升级点 + updateSystemMediaInfo(currentTime: currentTime, duration: duration) + + // 通知Flutter层 + let currentPosition = Int(currentTime * 1000) + let totalDuration = Int(duration * 1000) + let progress = totalDuration > 0 ? Float(currentTime / duration) : 0.0 + + flutterChannel?.invokeMethod("onMediaSessionProgress", arguments: [ + "currentIdInternal": currentIdInternal ?? 0, + "currentPosition": currentPosition, + "duration": totalDuration, + "progress": progress + ]) } } + + /// 立即更新进度信息(用于seekTo后) + private func updateProgressInfoImmediately() { + guard let player = player, let currentItem = player.currentItem else { return } + + let currentTime = CMTimeGetSeconds(player.currentTime()) + let duration = CMTimeGetSeconds(currentItem.duration) + + if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { + updateSystemMediaInfo(currentTime: currentTime, duration: duration) + } + } + + /// 更新系统媒体信息 + private func updateSystemMediaInfo(currentTime: Double, duration: Double) { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + + // MARK: - 播放完成处理 + private func setupPlayerObservers() { + guard let player = player else { return } + + NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: player.currentItem, + queue: .main + ) { [weak self] _ in + self?.stopProgressTimer() + self?.playNextOrStop() + } + } + + // MARK: - 播放项观察者设置 + private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) { + // 移除之前的观察者 + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: nil) + + // 监听播放项状态 + playerItem.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil) + playerItem.addObserver(self, forKeyPath: "duration", options: [.new], context: nil) + playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil) + + // 播放完成通知 + NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: playerItem, + queue: .main + ) { [weak self] _ in + print("歌曲播放完成") + self?.stopProgressTimer() + self?.playNextOrStop() + } + + // 播放失败通知 + NotificationCenter.default.addObserver( + forName: .AVPlayerItemFailedToPlayToEndTime, + object: playerItem, + queue: .main + ) { [weak self] notification in + print("播放失败: \(notification)") + self?.isPlaying = false + self?.updateMediaSessionPlaybackState(isPlaying: false) + } + + // 存储歌曲信息,用于状态变化时更新 + objc_setAssociatedObject(playerItem, "songInfo", song, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} +// MARK: - KVO 观察者 +override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let playerItem = object as? AVPlayerItem else { return } + + switch keyPath { + case "status": + switch playerItem.status { + case .readyToPlay: + print("播放项准备就绪,开始播放") + + // 获取关联的歌曲信息 + if let song = objc_getAssociatedObject(playerItem, "songInfo") as? [String: Any] { + updateNowPlayingInfo(song: song) + } + + // 开始播放 + player?.play() + isPlaying = true + updateMediaSessionPlaybackState(isPlaying: true) + startProgressTimer() + + case .failed: + print("播放项加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")") + isPlaying = false + updateMediaSessionPlaybackState(isPlaying: false) + + case .unknown: + print("播放项状态未知") + + @unknown default: + break + } + + case "duration": + let duration = CMTimeGetSeconds(playerItem.duration) + if !duration.isNaN && !duration.isInfinite && duration > 0 { + print("获取到音频时长: \(duration) 秒") + // 更新系统媒体信息 + updateProgressInfoImmediately() + } + + case "loadedTimeRanges": + if let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue { + let loadedDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration)) + let totalDuration = CMTimeGetSeconds(playerItem.duration) + if !totalDuration.isNaN && !totalDuration.isInfinite && totalDuration > 0 { + let loadedPercentage = loadedDuration / totalDuration * 100 + print("缓冲进度: \(String(format: "%.1f", loadedPercentage))%") + } + } + + default: + break + } } 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 index 61e1a844e..f4c99cd67 100644 --- 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 @@ -18,7 +18,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { switch call.method { case "play": if let args = call.arguments as? [String: Any], - let song = args["song"] as? [String: String] { + let song = args["song"] as? [String: Any] { MusicService.shared.play(song: song) result(nil) } else { @@ -27,8 +27,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { 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) + let songs = args["songs"] as? [[String: Any]], !songs.isEmpty { MusicService.shared.playPlaylist(songs: songs) result(nil) } else { @@ -44,7 +43,6 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { result(nil) case "stop": -// MusicServiceStarter.startService(command: "stop") MusicService.shared.stop() result(nil) @@ -62,7 +60,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { let currentUrl = MusicService.shared.getCurrentUrlString() let status: [String: Any] = [ "isPlaying": isPlaying, - "url": currentUrl != nil ? currentUrl! : NSNull() + "id": currentUrl ?? NSNull() ] result(status) @@ -76,8 +74,35 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { MusicService.shared.seekTo(positionMs: position) result(nil) } else { - result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or invalid 'position'", details: nil)) + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing 'position' parameter", details: nil)) } + + case "updateMediaSessionProgress": + let currentPosition = (call.arguments as? [String: Any])?["currentPosition"] as? Int ?? 0 + let duration = (call.arguments as? [String: Any])?["duration"] as? Int ?? 0 + let progress = (call.arguments as? [String: Any])?["progress"] as? Double ?? 0.0 + MusicService.shared.updateMediaSessionProgress(currentPosition: currentPosition, duration: duration, progress: Float(progress)) + result(nil) + + case "enableMediaSessionCallbacks": + if let channel = MusicServicePlugin.channel { + MusicService.shared.enableMediaSessionCallbacks(channel: channel) + } + result(nil) + + case "updateMediaSessionMetadata": + if let args = call.arguments as? [String: Any], + let song = args["song"] as? [String: Any] { + MusicService.shared.updateMediaSessionMetadata(song: song) + result(nil) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Song data is required", details: nil)) + } + + case "updateMediaSessionPlaybackState": + let isPlaying = (call.arguments as? [String: Any])?["isPlaying"] as? Bool ?? false + MusicService.shared.updateMediaSessionPlaybackState(isPlaying: isPlaying) + result(nil) default: result(FlutterMethodNotImplemented)