Browse Source

上传聊天代码和音乐服务代码

weicu
liwei1dao 1 year ago
parent
commit
5e2278ffe7
  1. 272
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  2. 145
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift
  3. 112
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  4. 404
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  5. 37
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift

272
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 {

145
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"
])
}
}
}
}

112
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<Void, Never>?
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<ChatStreamResult, Error> = 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)"
}
}
}

404
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
}
}

37
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)

Loading…
Cancel
Save