682 lines
25 KiB
Objective-C
682 lines
25 KiB
Objective-C
//
|
||
// KeyboardViewController+Chat.m
|
||
// CustomKeyboard
|
||
//
|
||
// Created by Codex on 2026/02/22.
|
||
//
|
||
|
||
#import "KeyboardViewController+Private.h"
|
||
|
||
#import "KBChatLimitPopView.h"
|
||
#import "KBChatMessage.h"
|
||
#import "KBChatPanelView.h"
|
||
#import "KBFullAccessManager.h"
|
||
#import "KBHostAppLauncher.h"
|
||
#import "KBInputBufferManager.h"
|
||
#import "KBNetworkManager.h"
|
||
#import "KBVM.h"
|
||
#import "Masonry.h"
|
||
|
||
#import <AVFoundation/AVFoundation.h>
|
||
|
||
static const NSUInteger kKBChatMessageLimit = 6;
|
||
|
||
@implementation KeyboardViewController (Chat)
|
||
|
||
#pragma mark - KBChatPanelViewDelegate
|
||
|
||
- (void)chatPanelView:(KBChatPanelView *)view didSendText:(NSString *)text {
|
||
NSString *trim =
|
||
[text stringByTrimmingCharactersInSet:
|
||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
if (trim.length == 0) {
|
||
return;
|
||
}
|
||
[self kb_sendChatText:trim];
|
||
}
|
||
|
||
- (void)chatPanelView:(KBChatPanelView *)view
|
||
didTapMessage:(KBChatMessage *)message {
|
||
if (message.audioFilePath.length == 0) {
|
||
return;
|
||
}
|
||
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||
}
|
||
|
||
- (void)chatPanelView:(KBChatPanelView *)view
|
||
didTapVoiceButtonForMessage:(KBChatMessage *)message {
|
||
if (!message)
|
||
return;
|
||
|
||
// 如果有 audioData,直接播放
|
||
if (message.audioData && message.audioData.length > 0) {
|
||
[self kb_playChatAudioData:message.audioData];
|
||
return;
|
||
}
|
||
|
||
// 如果有 audioFilePath,播放文件
|
||
if (message.audioFilePath.length > 0) {
|
||
[self kb_playChatAudioAtPath:message.audioFilePath];
|
||
return;
|
||
}
|
||
|
||
NSLog(@"[Keyboard] 没有音频数据可播放");
|
||
}
|
||
|
||
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
||
// 清空 chatPanelView 内部的消息
|
||
[view kb_reloadWithMessages:@[]];
|
||
if (self.chatAudioPlayer.isPlaying) {
|
||
[self.chatAudioPlayer stop];
|
||
}
|
||
self.chatAudioPlayer = nil;
|
||
[self kb_setPanelMode:KBKeyboardPanelModeMain animated:YES];
|
||
}
|
||
|
||
#pragma mark - Chat Helpers
|
||
|
||
- (void)kb_handleChatSendAction {
|
||
if (!self.chatPanelVisible) {
|
||
return;
|
||
}
|
||
[[KBInputBufferManager shared]
|
||
refreshFromProxyIfPossible:self.textDocumentProxy];
|
||
NSString *fullText = [KBInputBufferManager shared].liveText ?: @"";
|
||
|
||
// 去掉打开聊天面板前宿主输入框里已有的基线文本,只取新增部分
|
||
NSString *baseline = self.chatPanelBaselineText ?: @"";
|
||
NSString *rawText = fullText;
|
||
if (baseline.length > 0 && [fullText hasPrefix:baseline]) {
|
||
rawText = [fullText substringFromIndex:baseline.length];
|
||
}
|
||
|
||
NSString *trim =
|
||
[rawText stringByTrimmingCharactersInSet:
|
||
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||
if (trim.length == 0) {
|
||
[KBHUD showInfo:KBLocalized(@"请输入内容")];
|
||
return;
|
||
}
|
||
[self kb_sendChatText:trim];
|
||
// 只清除新增的文本,保留基线文本
|
||
[self kb_clearHostInputForText:rawText];
|
||
}
|
||
|
||
- (void)kb_sendChatText:(NSString *)text {
|
||
if (text.length == 0) {
|
||
return;
|
||
}
|
||
NSLog(@"[KB] 发送消息: %@", text);
|
||
|
||
KBChatMessage *outgoing = [KBChatMessage userMessageWithText:text];
|
||
outgoing.avatarURL = [self kb_sharedUserAvatarURL];
|
||
[self.chatPanelView kb_addUserMessage:text];
|
||
[self kb_prefetchAvatarForMessage:outgoing];
|
||
|
||
if (![[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view]) {
|
||
[KBHUD showInfo:KBLocalized(@"请开启完全访问后使用")];
|
||
return;
|
||
}
|
||
|
||
// 添加 loading 消息
|
||
[self.chatPanelView kb_addLoadingAssistantMessage];
|
||
|
||
// 调用新的聊天接口
|
||
[self kb_requestChatMessageWithContent:text];
|
||
}
|
||
|
||
#pragma mark - Chat Limit Pop
|
||
|
||
- (void)kb_showChatLimitPopWithMessage:(NSString *)message {
|
||
[self kb_dismissChatLimitPop];
|
||
|
||
UIControl *mask = [[UIControl alloc] init];
|
||
mask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
|
||
mask.alpha = 0.0;
|
||
[mask addTarget:self
|
||
action:@selector(kb_dismissChatLimitPop)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self.contentView addSubview:mask];
|
||
[mask mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.contentView);
|
||
}];
|
||
|
||
CGFloat width = 252.0;
|
||
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0;
|
||
KBChatLimitPopView *content =
|
||
[[KBChatLimitPopView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
|
||
content.message = message ?: @"";
|
||
content.delegate = self;
|
||
[mask addSubview:content];
|
||
[content mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.center.equalTo(mask);
|
||
make.width.mas_equalTo(width);
|
||
make.height.mas_equalTo(height);
|
||
}];
|
||
|
||
self.chatLimitMaskView = mask;
|
||
[self.contentView bringSubviewToFront:mask];
|
||
[UIView animateWithDuration:0.18
|
||
animations:^{
|
||
mask.alpha = 1.0;
|
||
}];
|
||
}
|
||
|
||
- (void)kb_dismissChatLimitPop {
|
||
if (!self.chatLimitMaskView) {
|
||
return;
|
||
}
|
||
UIControl *mask = self.chatLimitMaskView;
|
||
self.chatLimitMaskView = nil;
|
||
[UIView animateWithDuration:0.15
|
||
animations:^{
|
||
mask.alpha = 0.0;
|
||
}
|
||
completion:^(__unused BOOL finished) {
|
||
[mask removeFromSuperview];
|
||
}];
|
||
}
|
||
|
||
- (void)kb_clearHostInputForText:(NSString *)text {
|
||
if (text.length == 0) {
|
||
return;
|
||
}
|
||
NSUInteger count = [self kb_composedCharacterCountForString:text];
|
||
for (NSUInteger i = 0; i < count; i++) {
|
||
[self.textDocumentProxy deleteBackward];
|
||
}
|
||
[[KBInputBufferManager shared] clearAllLiveText];
|
||
[self kb_clearCurrentWord];
|
||
}
|
||
|
||
- (NSUInteger)kb_composedCharacterCountForString:(NSString *)text {
|
||
if (text.length == 0) {
|
||
return 0;
|
||
}
|
||
__block NSUInteger count = 0;
|
||
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
|
||
options:NSStringEnumerationByComposedCharacterSequences
|
||
usingBlock:^(__unused NSString *substring,
|
||
__unused NSRange substringRange,
|
||
__unused NSRange enclosingRange,
|
||
__unused BOOL *stop) {
|
||
count += 1;
|
||
}];
|
||
return count;
|
||
}
|
||
|
||
- (NSString *)kb_sharedUserAvatarURL {
|
||
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||
NSString *url = [ud stringForKey:AppGroup_UserAvatarURL];
|
||
return url ?: @"";
|
||
}
|
||
|
||
- (void)kb_prefetchAvatarForMessage:(KBChatMessage *)message {
|
||
if (!message || message.avatarImage) {
|
||
return;
|
||
}
|
||
NSString *urlString = message.avatarURL ?: @"";
|
||
if (urlString.length == 0) {
|
||
return;
|
||
}
|
||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||
return;
|
||
}
|
||
__weak typeof(self) weakSelf = self;
|
||
[[KBVM shared] downloadAvatarFromURL:urlString
|
||
completion:^(UIImage *image, NSError *error) {
|
||
__strong typeof(weakSelf) self = weakSelf;
|
||
if (!self || !image)
|
||
return;
|
||
|
||
message.avatarImage = image;
|
||
[self kb_reloadChatRowForMessage:message];
|
||
}];
|
||
}
|
||
|
||
- (void)kb_reloadChatRowForMessage:(KBChatMessage *)message {
|
||
// 头像预加载完成后不需要刷新表格
|
||
// 因为键盘扩展的聊天面板不显示头像,所以这里直接返回
|
||
// 如果将来需要显示头像,可以只刷新特定行而不是整个表格
|
||
}
|
||
|
||
- (void)kb_requestChatAudioForText:(NSString *)text {
|
||
NSString *mockPath = [self kb_mockChatAudioPath];
|
||
if (mockPath.length > 0) {
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)),
|
||
dispatch_get_main_queue(), ^{
|
||
NSString *displayText = KBLocalized(@"语音回复");
|
||
KBChatMessage *incoming =
|
||
[KBChatMessage messageWithText:displayText
|
||
outgoing:NO
|
||
audioFilePath:mockPath];
|
||
incoming.displayName = KBLocalized(@"AI助手");
|
||
[self kb_appendChatMessage:incoming];
|
||
[self kb_playChatAudioAtPath:mockPath];
|
||
});
|
||
return;
|
||
}
|
||
NSDictionary *payload = @{@"message" : text ?: @""};
|
||
__weak typeof(self) weakSelf = self;
|
||
[[KBNetworkManager shared] POST:API_AI_TALK
|
||
jsonBody:payload
|
||
headers:nil
|
||
completion:^(NSDictionary *json, NSURLResponse *response,
|
||
NSError *error) {
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
__strong typeof(weakSelf) self = weakSelf;
|
||
if (!self) {
|
||
return;
|
||
}
|
||
if (error) {
|
||
NSString *tip = error.localizedDescription
|
||
?: KBLocalized(@"请求失败");
|
||
[KBHUD showInfo:tip];
|
||
return;
|
||
}
|
||
NSString *displayText =
|
||
[self kb_chatTextFromJSON:json];
|
||
NSString *audioURL =
|
||
[self kb_chatAudioURLFromJSON:json];
|
||
NSString *audioBase64 =
|
||
[self kb_chatAudioBase64FromJSON:json];
|
||
if (audioURL.length > 0) {
|
||
[self kb_downloadChatAudioFromURL:audioURL
|
||
displayText:displayText];
|
||
return;
|
||
}
|
||
if (audioBase64.length > 0) {
|
||
NSData *data = [[NSData alloc]
|
||
initWithBase64EncodedString:audioBase64
|
||
options:0];
|
||
if (data.length == 0) {
|
||
[KBHUD showInfo:KBLocalized(@"音频数据解析失败")];
|
||
return;
|
||
}
|
||
[self kb_handleChatAudioData:data
|
||
fileExtension:@"m4a"
|
||
displayText:displayText];
|
||
return;
|
||
}
|
||
[KBHUD showInfo:KBLocalized(@"未获取到音频文件")];
|
||
});
|
||
}];
|
||
}
|
||
|
||
#pragma mark - New Chat API (with typewriter effect and audio preload)
|
||
|
||
/// 调用新的聊天接口(返回文本和 audioId)
|
||
- (void)kb_requestChatMessageWithContent:(NSString *)content {
|
||
if (content.length == 0) {
|
||
[self.chatPanelView kb_removeLoadingAssistantMessage];
|
||
return;
|
||
}
|
||
|
||
NSInteger companionId = [[KBVM shared] selectedCompanionIdFromAppGroup];
|
||
NSLog(@"[KB] 请求聊天: companionId=%ld", (long)companionId);
|
||
|
||
__weak typeof(self) weakSelf = self;
|
||
[[KBVM shared] sendChatMessageWithContent:content
|
||
companionId:companionId
|
||
completion:^(KBChatResponse *response) {
|
||
__strong typeof(weakSelf) self = weakSelf;
|
||
if (!self)
|
||
return;
|
||
|
||
if (response.code != 0) {
|
||
if (response.code == 50030) {
|
||
NSLog(@"[KB] ⚠️ 次数用尽: %@",
|
||
response.message);
|
||
[self.chatPanelView
|
||
kb_removeLoadingAssistantMessage];
|
||
[self kb_showChatLimitPopWithMessage:
|
||
response.message];
|
||
return;
|
||
}
|
||
NSLog(@"[KB] ❌ 请求失败: %@",
|
||
response.message);
|
||
[self.chatPanelView
|
||
kb_removeLoadingAssistantMessage];
|
||
[KBHUD showInfo:response.message
|
||
?: KBLocalized(@"请求失败")];
|
||
return;
|
||
}
|
||
|
||
NSLog(@"[KB] ✅ 收到回复: %@",
|
||
response.data.aiResponse);
|
||
|
||
if (response.data.aiResponse.length == 0) {
|
||
[self.chatPanelView
|
||
kb_removeLoadingAssistantMessage];
|
||
[KBHUD showInfo:KBLocalized(@"未获取到回复内容")];
|
||
return;
|
||
}
|
||
|
||
// 添加 AI 消息(带打字机效果)
|
||
NSLog(@"[KB] 准备添加 AI 消息");
|
||
[self.chatPanelView
|
||
kb_addAssistantMessage:response.data.aiResponse
|
||
audioId:response.data.audioId];
|
||
NSLog(@"[KB] AI 消息添加完成");
|
||
|
||
// 如果有 audioId,开始预加载音频
|
||
if (response.data.audioId.length > 0) {
|
||
[self kb_preloadAudioWithAudioId:
|
||
response.data.audioId];
|
||
}
|
||
}];
|
||
}
|
||
|
||
/// 从 AppGroup 获取选中的 persona companionId
|
||
- (NSInteger)kb_selectedCompanionId {
|
||
return [[KBVM shared] selectedCompanionIdFromAppGroup];
|
||
}
|
||
|
||
#pragma mark - Audio Preload
|
||
|
||
/// 预加载音频(轮询获取 audioURL)
|
||
- (void)kb_preloadAudioWithAudioId:(NSString *)audioId {
|
||
if (audioId.length == 0)
|
||
return;
|
||
|
||
NSLog(@"[Keyboard] 开始预加载音频,audioId: %@", audioId);
|
||
|
||
__weak typeof(self) weakSelf = self;
|
||
[[KBVM shared] pollAudioURLWithAudioId:audioId
|
||
maxRetries:10
|
||
interval:1.0
|
||
completion:^(KBAudioResponse *response) {
|
||
__strong typeof(weakSelf) self = weakSelf;
|
||
if (!self)
|
||
return;
|
||
|
||
if (!response.success ||
|
||
response.audioURL.length == 0) {
|
||
NSLog(@"[Keyboard] ❌ 预加载音频 URL 获取失败: %@",
|
||
response.errorMessage);
|
||
return;
|
||
}
|
||
|
||
NSLog(@"[Keyboard] ✅ 预加载音频 URL 获取成功");
|
||
|
||
// 下载音频
|
||
[[KBVM shared]
|
||
downloadAudioFromURL:response.audioURL
|
||
completion:^(
|
||
KBAudioResponse *audioResponse) {
|
||
if (!audioResponse.success) {
|
||
NSLog(@"[Keyboard] ❌ 预加载音频下载失败: %@",
|
||
audioResponse.errorMessage);
|
||
return;
|
||
}
|
||
|
||
// 更新最后一条 AI 消息的音频数据
|
||
[self.chatPanelView
|
||
kb_updateLastAssistantMessageWithAudioData:
|
||
audioResponse.audioData
|
||
duration:
|
||
audioResponse.duration];
|
||
NSLog(@"[Keyboard] ✅ 预加载音频完成,音频时长: %.2f秒",
|
||
audioResponse.duration);
|
||
}];
|
||
}];
|
||
}
|
||
|
||
- (void)kb_downloadChatAudioFromURL:(NSString *)audioURL
|
||
displayText:(NSString *)displayText {
|
||
__weak typeof(self) weakSelf = self;
|
||
[[KBVM shared] downloadAudioFromURL:audioURL
|
||
completion:^(KBAudioResponse *response) {
|
||
__strong typeof(weakSelf) self = weakSelf;
|
||
if (!self)
|
||
return;
|
||
|
||
if (!response.success) {
|
||
[KBHUD showInfo:response.errorMessage
|
||
?: KBLocalized(@"下载失败")];
|
||
return;
|
||
}
|
||
|
||
if (!response.audioData ||
|
||
response.audioData.length == 0) {
|
||
[KBHUD showInfo:KBLocalized(@"未获取到音频数据")];
|
||
return;
|
||
}
|
||
|
||
NSString *ext = @"m4a";
|
||
NSURL *url = [NSURL URLWithString:audioURL];
|
||
if (url.pathExtension.length > 0) {
|
||
ext = url.pathExtension;
|
||
}
|
||
[self kb_handleChatAudioData:response.audioData
|
||
fileExtension:ext
|
||
displayText:displayText];
|
||
}];
|
||
}
|
||
|
||
- (void)kb_handleChatAudioData:(NSData *)data
|
||
fileExtension:(NSString *)extension
|
||
displayText:(NSString *)displayText {
|
||
if (data.length == 0) {
|
||
[KBHUD showInfo:KBLocalized(@"音频数据为空")];
|
||
return;
|
||
}
|
||
NSString *ext = extension.length > 0 ? extension : @"m4a";
|
||
NSString *fileName = [NSString
|
||
stringWithFormat:@"kb_chat_%@.%@",
|
||
@((long long)([NSDate date].timeIntervalSince1970 *
|
||
1000)),
|
||
ext];
|
||
NSString *filePath =
|
||
[NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
|
||
if (![data writeToFile:filePath atomically:YES]) {
|
||
[KBHUD showInfo:KBLocalized(@"音频保存失败")];
|
||
return;
|
||
}
|
||
NSString *text =
|
||
displayText.length > 0 ? displayText : KBLocalized(@"语音消息");
|
||
KBChatMessage *incoming =
|
||
[KBChatMessage messageWithText:text outgoing:NO audioFilePath:filePath];
|
||
incoming.displayName = KBLocalized(@"AI助手");
|
||
[self kb_appendChatMessage:incoming];
|
||
}
|
||
|
||
- (void)kb_appendChatMessage:(KBChatMessage *)message {
|
||
if (!message) {
|
||
return;
|
||
}
|
||
[self.chatMessages addObject:message];
|
||
if (self.chatMessages.count > kKBChatMessageLimit) {
|
||
NSUInteger overflow = self.chatMessages.count - kKBChatMessageLimit;
|
||
NSArray<KBChatMessage *> *removed =
|
||
[self.chatMessages subarrayWithRange:NSMakeRange(0, overflow)];
|
||
[self.chatMessages removeObjectsInRange:NSMakeRange(0, overflow)];
|
||
for (KBChatMessage *msg in removed) {
|
||
if (msg.audioFilePath.length > 0) {
|
||
NSString *tmpRoot = NSTemporaryDirectory();
|
||
if (tmpRoot.length > 0 && [msg.audioFilePath hasPrefix:tmpRoot]) {
|
||
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
|
||
error:nil];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
[self.chatPanelView kb_reloadWithMessages:self.chatMessages];
|
||
}
|
||
|
||
- (NSString *)kb_mockChatAudioPath {
|
||
NSString *path = [[NSBundle mainBundle] pathForResource:@"ai_test"
|
||
ofType:@"m4a"];
|
||
return path ?: @"";
|
||
}
|
||
|
||
- (NSString *)kb_chatTextFromJSON:(NSDictionary *)json {
|
||
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||
NSString *text =
|
||
[self kb_stringValueInDict:data keys:@[ @"text", @"message", @"content" ]];
|
||
if (text.length == 0) {
|
||
text = [self kb_stringValueInDict:json
|
||
keys:@[ @"text", @"message", @"content" ]];
|
||
}
|
||
return text ?: @"";
|
||
}
|
||
|
||
- (NSString *)kb_chatAudioURLFromJSON:(NSDictionary *)json {
|
||
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||
NSArray<NSString *> *keys =
|
||
@[ @"audioUrl", @"audioURL", @"audio_url", @"url", @"fileUrl",
|
||
@"file_url", @"audioFileUrl", @"audio_file_url" ];
|
||
NSString *url = [self kb_stringValueInDict:data keys:keys];
|
||
if (url.length == 0) {
|
||
url = [self kb_stringValueInDict:json keys:keys];
|
||
}
|
||
return url ?: @"";
|
||
}
|
||
|
||
- (NSString *)kb_chatAudioBase64FromJSON:(NSDictionary *)json {
|
||
NSDictionary *data = [self kb_chatDataDictionaryFromJSON:json];
|
||
NSArray<NSString *> *keys =
|
||
@[ @"audioBase64", @"audio_base64", @"audioData", @"audio_data",
|
||
@"base64" ];
|
||
NSString *b64 = [self kb_stringValueInDict:data keys:keys];
|
||
if (b64.length == 0) {
|
||
b64 = [self kb_stringValueInDict:json keys:keys];
|
||
}
|
||
return b64 ?: @"";
|
||
}
|
||
|
||
- (NSDictionary *)kb_chatDataDictionaryFromJSON:(NSDictionary *)json {
|
||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||
return @{};
|
||
}
|
||
id dataObj = json[@"data"] ?: json[@"result"] ?: json[@"response"];
|
||
if ([dataObj isKindOfClass:[NSDictionary class]]) {
|
||
return (NSDictionary *)dataObj;
|
||
}
|
||
return @{};
|
||
}
|
||
|
||
- (NSString *)kb_stringValueInDict:(NSDictionary *)dict
|
||
keys:(NSArray<NSString *> *)keys {
|
||
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||
return @"";
|
||
}
|
||
for (NSString *key in keys) {
|
||
id value = dict[key];
|
||
if ([value isKindOfClass:[NSString class]] &&
|
||
((NSString *)value).length > 0) {
|
||
return (NSString *)value;
|
||
}
|
||
}
|
||
return @"";
|
||
}
|
||
|
||
- (void)kb_playChatAudioAtPath:(NSString *)path {
|
||
if (path.length == 0) {
|
||
return;
|
||
}
|
||
NSURL *url = [NSURL fileURLWithPath:path];
|
||
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
||
[KBHUD showInfo:KBLocalized(@"音频文件不存在")];
|
||
return;
|
||
}
|
||
|
||
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
|
||
NSURL *currentURL = self.chatAudioPlayer.url;
|
||
if ([currentURL isEqual:url]) {
|
||
[self.chatAudioPlayer stop];
|
||
self.chatAudioPlayer = nil;
|
||
return;
|
||
}
|
||
[self.chatAudioPlayer stop];
|
||
self.chatAudioPlayer = nil;
|
||
}
|
||
|
||
NSError *sessionError = nil;
|
||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
|
||
[session setCategory:AVAudioSessionCategoryPlayback
|
||
withOptions:AVAudioSessionCategoryOptionDuckOthers
|
||
error:&sessionError];
|
||
} else {
|
||
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
|
||
}
|
||
[session setActive:YES error:nil];
|
||
|
||
NSError *playerError = nil;
|
||
AVAudioPlayer *player =
|
||
[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&playerError];
|
||
if (playerError || !player) {
|
||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
||
return;
|
||
}
|
||
self.chatAudioPlayer = player;
|
||
[player prepareToPlay];
|
||
[player play];
|
||
}
|
||
|
||
/// 播放音频数据
|
||
- (void)kb_playChatAudioData:(NSData *)audioData {
|
||
if (!audioData || audioData.length == 0) {
|
||
NSLog(@"[Keyboard] 音频数据为空");
|
||
return;
|
||
}
|
||
|
||
// 如果正在播放,先停止
|
||
if (self.chatAudioPlayer && self.chatAudioPlayer.isPlaying) {
|
||
[self.chatAudioPlayer stop];
|
||
self.chatAudioPlayer = nil;
|
||
}
|
||
|
||
// 配置音频会话
|
||
NSError *sessionError = nil;
|
||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||
if ([session respondsToSelector:@selector(setCategory:options:error:)]) {
|
||
[session setCategory:AVAudioSessionCategoryPlayback
|
||
withOptions:AVAudioSessionCategoryOptionDuckOthers
|
||
error:&sessionError];
|
||
} else {
|
||
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
|
||
}
|
||
[session setActive:YES error:nil];
|
||
|
||
// 创建播放器
|
||
NSError *playerError = nil;
|
||
AVAudioPlayer *player =
|
||
[[AVAudioPlayer alloc] initWithData:audioData error:&playerError];
|
||
if (playerError || !player) {
|
||
NSLog(@"[Keyboard] 音频播放器初始化失败: %@",
|
||
playerError.localizedDescription);
|
||
[KBHUD showInfo:KBLocalized(@"音频播放失败")];
|
||
return;
|
||
}
|
||
|
||
self.chatAudioPlayer = player;
|
||
player.volume = 1.0;
|
||
[player prepareToPlay];
|
||
[player play];
|
||
|
||
NSLog(@"[Keyboard] 开始播放音频,时长: %.2f秒", player.duration);
|
||
}
|
||
|
||
#pragma mark - KBChatLimitPopViewDelegate
|
||
|
||
- (void)chatLimitPopViewDidTapCancel:(KBChatLimitPopView *)view {
|
||
[self kb_dismissChatLimitPop];
|
||
}
|
||
|
||
- (void)chatLimitPopViewDidTapRecharge:(KBChatLimitPopView *)view {
|
||
[self kb_dismissChatLimitPop];
|
||
NSString *urlString =
|
||
[NSString stringWithFormat:@"%@://recharge?src=keyboard&vipType=svip",
|
||
KB_APP_SCHEME];
|
||
NSURL *scheme = [NSURL URLWithString:urlString];
|
||
BOOL success = [KBHostAppLauncher openHostAppURL:scheme
|
||
fromResponder:self.view];
|
||
if (!success) {
|
||
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
|
||
}
|
||
}
|
||
|
||
@end
|