1
This commit is contained in:
312
Shared/KBVoiceRecordManager.m
Normal file
312
Shared/KBVoiceRecordManager.m
Normal file
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// KBVoiceRecordManager.m
|
||||
//
|
||||
|
||||
#import "KBVoiceRecordManager.h"
|
||||
#import "KBConfig.h"
|
||||
#import "KBVoiceBridgeNotification.h"
|
||||
#import "KBHUD.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
static void KBVoiceBridgeDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo);
|
||||
|
||||
@interface KBVoiceRecordManager () <AVAudioRecorderDelegate>
|
||||
@property (nonatomic, strong) AVAudioRecorder *recorder;
|
||||
@property (nonatomic, strong) NSURL *recordURL;
|
||||
@property (nonatomic, assign, readwrite, getter=isRecording) BOOL recording;
|
||||
@property (nonatomic, assign) BOOL pendingStart;
|
||||
@end
|
||||
|
||||
@implementation KBVoiceRecordManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBVoiceRecordManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBVoiceRecordManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBVoiceBridgeDarwinCallback,
|
||||
(__bridge CFStringRef)KBDarwinVoiceStartRequest,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
KBVoiceBridgeDarwinCallback,
|
||||
(__bridge CFStringRef)KBDarwinVoiceStopRequest,
|
||||
NULL,
|
||||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
(__bridge CFStringRef)KBDarwinVoiceStartRequest,
|
||||
NULL);
|
||||
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge const void *)(self),
|
||||
(__bridge CFStringRef)KBDarwinVoiceStopRequest,
|
||||
NULL);
|
||||
}
|
||||
|
||||
static void KBVoiceBridgeDarwinCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
|
||||
KBVoiceRecordManager *self = (__bridge KBVoiceRecordManager *)observer;
|
||||
if (!self) { return; }
|
||||
NSString *n = (__bridge NSString *)name;
|
||||
NSLog(@"[KBVoiceBridge][App] Darwin received: %@", n);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([n isEqualToString:KBDarwinVoiceStartRequest]) {
|
||||
[self startRecording];
|
||||
} else if ([n isEqualToString:KBDarwinVoiceStopRequest]) {
|
||||
[self stopRecording];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)startRecording {
|
||||
if (self.isRecording) {
|
||||
NSLog(@"[KBVoiceBridge][App] startRecording already recording, stop then restart");
|
||||
[self stopRecording];
|
||||
// return;
|
||||
}
|
||||
NSLog(@"[KBVoiceBridge][App] startRecording begin");
|
||||
[self kb_clearSharedState];
|
||||
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
AVAudioSessionRecordPermission permission = session.recordPermission;
|
||||
if (permission == AVAudioSessionRecordPermissionDenied) {
|
||||
NSLog(@"[KBVoiceBridge][App] recordPermission denied");
|
||||
[self kb_postFailed:KBLocalized(@"麦克风权限未开启")];
|
||||
return;
|
||||
}
|
||||
if (permission == AVAudioSessionRecordPermissionUndetermined) {
|
||||
NSLog(@"[KBVoiceBridge][App] recordPermission undetermined, requesting");
|
||||
self.pendingStart = YES;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[session requestRecordPermission:^(BOOL granted) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
if (!self) { return; }
|
||||
if (!self.pendingStart) { return; }
|
||||
self.pendingStart = NO;
|
||||
if (!granted) {
|
||||
NSLog(@"[KBVoiceBridge][App] recordPermission request denied");
|
||||
[self kb_postFailed:KBLocalized(@"麦克风权限未开启")];
|
||||
return;
|
||||
}
|
||||
NSLog(@"[KBVoiceBridge][App] recordPermission request granted");
|
||||
[self startRecording];
|
||||
});
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
mode:AVAudioSessionModeDefault
|
||||
options:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth
|
||||
error:&error];
|
||||
} else {
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
|
||||
}
|
||||
if (error) {
|
||||
NSLog(@"[KBVoiceBridge][App] setCategory error: %@", error.localizedDescription);
|
||||
[self kb_postFailed:KBLocalized(@"麦克风初始化失败")];
|
||||
return;
|
||||
}
|
||||
[session setActive:YES error:&error];
|
||||
if (error) {
|
||||
NSLog(@"[KBVoiceBridge][App] setActive error: %@", error.localizedDescription);
|
||||
[self kb_postFailed:error.localizedDescription ?: KBLocalized(@"麦克风启动失败")];
|
||||
return;
|
||||
}
|
||||
if (!session.isInputAvailable) {
|
||||
NSLog(@"[KBVoiceBridge][App] input not available");
|
||||
[self kb_postFailed:KBLocalized(@"麦克风不可用")];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *aacURL = [self kb_voiceFileURLWithExtension:@"m4a"];
|
||||
if (!aacURL) {
|
||||
NSLog(@"[KBVoiceBridge][App] app group not configured");
|
||||
[self kb_postFailed:KBLocalized(@"App Group 未配置,无法共享录音")];
|
||||
return;
|
||||
}
|
||||
NSDictionary *aacSettings = [self kb_voiceRecordSettingsAAC];
|
||||
if ([self kb_tryStartRecorderWithSettings:aacSettings fileURL:aacURL error:&error]) {
|
||||
NSLog(@"[KBVoiceBridge][App] recorder started (aac) url=%@", aacURL);
|
||||
self.recordURL = aacURL;
|
||||
self.recording = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *pcmURL = [self kb_voiceFileURLWithExtension:@"caf"];
|
||||
if (!pcmURL) {
|
||||
NSLog(@"[KBVoiceBridge][App] app group not configured");
|
||||
[self kb_postFailed:KBLocalized(@"App Group 未配置,无法共享录音")];
|
||||
return;
|
||||
}
|
||||
NSDictionary *pcmSettings = [self kb_voiceRecordSettingsPCM];
|
||||
error = nil;
|
||||
if ([self kb_tryStartRecorderWithSettings:pcmSettings fileURL:pcmURL error:&error]) {
|
||||
NSLog(@"[KBVoiceBridge][App] recorder started (pcm) url=%@", pcmURL);
|
||||
self.recordURL = pcmURL;
|
||||
self.recording = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[KBVoiceBridge][App] recorder start failed: %@", error.localizedDescription);
|
||||
NSString *tip = error.localizedDescription ?: KBLocalized(@"录音启动失败,可能是系统限制或宿主 App 不允许录音");
|
||||
[self kb_postFailed:tip];
|
||||
}
|
||||
|
||||
- (void)stopRecording {
|
||||
self.pendingStart = NO;
|
||||
NSLog(@"[KBVoiceBridge][App] stopRecording");
|
||||
if (self.recorder.isRecording) {
|
||||
[self.recorder stop];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - AVAudioRecorderDelegate
|
||||
|
||||
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
|
||||
[[AVAudioSession sharedInstance] setActive:NO error:nil];
|
||||
NSLog(@"[KBVoiceBridge][App] finishRecording flag=%d url=%@", flag, recorder.url);
|
||||
NSURL *fileURL = recorder.url ?: self.recordURL;
|
||||
self.recorder = nil;
|
||||
self.recordURL = nil;
|
||||
self.recording = NO;
|
||||
|
||||
if (!flag || !fileURL) {
|
||||
[self kb_postFailed:KBLocalized(@"录音失败")];
|
||||
return;
|
||||
}
|
||||
|
||||
[self kb_saveSharedFileURL:fileURL];
|
||||
[self kb_postDarwin:KBDarwinVoiceReady];
|
||||
}
|
||||
|
||||
- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error {
|
||||
[[AVAudioSession sharedInstance] setActive:NO error:nil];
|
||||
NSLog(@"[KBVoiceBridge][App] encodeError: %@", error.localizedDescription);
|
||||
self.recorder = nil;
|
||||
self.recordURL = nil;
|
||||
self.recording = NO;
|
||||
[self kb_postFailed:error.localizedDescription ?: KBLocalized(@"录音失败")];
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (NSDictionary *)kb_voiceRecordSettingsAAC {
|
||||
return @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),
|
||||
AVSampleRateKey: @(16000),
|
||||
AVNumberOfChannelsKey: @(1),
|
||||
AVEncoderAudioQualityKey: @(AVAudioQualityMedium)};
|
||||
}
|
||||
|
||||
- (NSDictionary *)kb_voiceRecordSettingsPCM {
|
||||
return @{AVFormatIDKey: @(kAudioFormatLinearPCM),
|
||||
AVSampleRateKey: @(16000),
|
||||
AVNumberOfChannelsKey: @(1),
|
||||
AVLinearPCMBitDepthKey: @(16),
|
||||
AVLinearPCMIsFloatKey: @(NO),
|
||||
AVLinearPCMIsBigEndianKey: @(NO)};
|
||||
}
|
||||
|
||||
- (NSURL *)kb_voiceFileURLWithExtension:(NSString *)ext {
|
||||
NSURL *dirURL = [self kb_voiceDirectoryURL];
|
||||
if (!dirURL) {
|
||||
return nil;
|
||||
}
|
||||
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970] * 1000;
|
||||
NSString *safeExt = (ext.length > 0) ? ext : @"m4a";
|
||||
NSString *fileName = [NSString stringWithFormat:@"kb_voice_%lld.%@", (long long)ts, safeExt];
|
||||
return [dirURL URLByAppendingPathComponent:fileName];
|
||||
}
|
||||
|
||||
- (NSURL *)kb_voiceDirectoryURL {
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
||||
if (!containerURL) {
|
||||
return nil;
|
||||
}
|
||||
NSURL *dirURL = [containerURL URLByAppendingPathComponent:@"voice" isDirectory:YES];
|
||||
if (dirURL && ![[NSFileManager defaultManager] fileExistsAtPath:dirURL.path]) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtURL:dirURL withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
return dirURL;
|
||||
}
|
||||
|
||||
- (BOOL)kb_tryStartRecorderWithSettings:(NSDictionary *)settings fileURL:(NSURL *)fileURL error:(NSError **)error {
|
||||
AVAudioRecorder *recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:error];
|
||||
if (*error || !recorder) {
|
||||
NSLog(@"[KBVoiceBridge][App] create recorder failed: %@", (*error).localizedDescription);
|
||||
return NO;
|
||||
}
|
||||
recorder.delegate = self;
|
||||
recorder.meteringEnabled = YES;
|
||||
if (![recorder prepareToRecord]) {
|
||||
NSLog(@"[KBVoiceBridge][App] prepareToRecord failed");
|
||||
return NO;
|
||||
}
|
||||
if (![recorder record]) {
|
||||
NSLog(@"[KBVoiceBridge][App] record returned NO");
|
||||
return NO;
|
||||
}
|
||||
self.recorder = recorder;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSUserDefaults *)kb_voiceUserDefaults {
|
||||
return [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||
}
|
||||
|
||||
- (void)kb_clearSharedState {
|
||||
NSUserDefaults *ud = [self kb_voiceUserDefaults];
|
||||
[ud removeObjectForKey:KBVoiceBridgeFilePathKey];
|
||||
[ud removeObjectForKey:KBVoiceBridgeErrorKey];
|
||||
[ud removeObjectForKey:KBVoiceBridgeTimestampKey];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
- (void)kb_saveSharedFileURL:(NSURL *)fileURL {
|
||||
if (!fileURL) { return; }
|
||||
NSUserDefaults *ud = [self kb_voiceUserDefaults];
|
||||
[ud setObject:fileURL.path ?: @"" forKey:KBVoiceBridgeFilePathKey];
|
||||
[ud setObject:@([[NSDate date] timeIntervalSince1970]) forKey:KBVoiceBridgeTimestampKey];
|
||||
[ud removeObjectForKey:KBVoiceBridgeErrorKey];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
- (void)kb_saveSharedError:(NSString *)message {
|
||||
NSUserDefaults *ud = [self kb_voiceUserDefaults];
|
||||
[ud setObject:message ?: @"" forKey:KBVoiceBridgeErrorKey];
|
||||
[ud setObject:@([[NSDate date] timeIntervalSince1970]) forKey:KBVoiceBridgeTimestampKey];
|
||||
[ud removeObjectForKey:KBVoiceBridgeFilePathKey];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
- (void)kb_postFailed:(NSString *)message {
|
||||
[self kb_saveSharedError:message];
|
||||
[self kb_postDarwin:KBDarwinVoiceFailed];
|
||||
|
||||
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
|
||||
NSString *tip = message.length > 0 ? message : KBLocalized(@"录音失败");
|
||||
[KBHUD showInfo:tip];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_postDarwin:(NSString *)name {
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
|
||||
(__bridge CFStringRef)name,
|
||||
NULL, NULL, true);
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user