// // DeepgramStreamingManager.m // keyBoard // // Created by Mac on 2026/1/21. // #import "DeepgramStreamingManager.h" #import "AudioCaptureManager.h" #import "AudioSessionManager.h" #import "DeepgramWebSocketClient.h" #import static NSString *const kDeepgramStreamingManagerErrorDomain = @"DeepgramStreamingManager"; @interface DeepgramStreamingManager () @property(nonatomic, strong) AudioSessionManager *audioSession; @property(nonatomic, strong) AudioCaptureManager *audioCapture; @property(nonatomic, strong) DeepgramWebSocketClient *client; @property(nonatomic, strong) dispatch_queue_t stateQueue; @property(nonatomic, assign) BOOL streaming; @property(nonatomic, strong) NSMutableArray *pendingFrames; @property(nonatomic, assign) NSUInteger pendingFrameLimit; @property(nonatomic, assign) BOOL connecting; @property(nonatomic, assign) BOOL pendingStart; @property(nonatomic, assign) BOOL keepConnection; @property(nonatomic, strong) dispatch_source_t keepAliveTimer; @property(nonatomic, assign) NSInteger reconnectAttempts; @property(nonatomic, assign) NSInteger maxReconnectAttempts; @property(nonatomic, assign) BOOL reconnectScheduled; @property(nonatomic, assign) BOOL appInBackground; @property(nonatomic, assign) BOOL shouldReconnectOnForeground; @end @implementation DeepgramStreamingManager - (instancetype)init { self = [super init]; if (self) { _stateQueue = dispatch_queue_create("com.keyboard.aitalk.deepgram.manager", DISPATCH_QUEUE_SERIAL); _audioSession = [AudioSessionManager sharedManager]; _audioSession.delegate = self; _audioCapture = [[AudioCaptureManager alloc] init]; _audioCapture.delegate = self; /// 不需要自己处理音频转文本,改为录音结束把文件传递给后端 // _client = [[DeepgramWebSocketClient alloc] init]; // _client.delegate = self; _serverURL = @"wss://api.deepgram.com/v1/listen"; _encoding = @"linear16"; _sampleRate = 16000.0; _channels = 1; _punctuate = YES; _smartFormat = YES; _interimResults = YES; _pendingFrames = [[NSMutableArray alloc] init]; _pendingFrameLimit = 25; _connecting = NO; _pendingStart = NO; _keepConnection = NO; _reconnectAttempts = 0; _maxReconnectAttempts = 5; _reconnectScheduled = NO; _appInBackground = NO; _shouldReconnectOnForeground = NO; [self setupNotifications]; } return self; } - (void)dealloc { [self removeNotifications]; [self disconnectInternal]; } - (void)start { dispatch_async(self.stateQueue, ^{ if (self.appInBackground) { self.shouldReconnectOnForeground = YES; return; } self.keepConnection = YES; self.pendingStart = YES; self.reconnectAttempts = 0; if (self.apiKey.length == 0) { [self reportErrorWithMessage:@"Deepgram API key is required"]; return; } if (![self.audioSession hasMicrophonePermission]) { __weak typeof(self) weakSelf = self; [self.audioSession requestMicrophonePermission:^(BOOL granted) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } if (!granted) { [strongSelf reportErrorWithMessage:@"Microphone permission denied"]; return; } dispatch_async(strongSelf.stateQueue, ^{ [strongSelf start]; }); }]; return; } NSError *error = nil; if (![self.audioSession configureForConversation:&error]) { [self reportError:error]; return; } if (![self.audioSession activateSession:&error]) { [self reportError:error]; return; } if (![self.audioCapture isCapturing]) { NSError *captureError = nil; if (![self.audioCapture startCapture:&captureError]) { [self reportError:captureError]; return; } } NSLog(@"[DeepgramStreamingManager] Start streaming, server: %@", self.serverURL); if (self.client.isConnected) { [self beginStreamingIfReady]; return; } [self connectIfNeeded]; }); } - (void)prepareConnection { dispatch_async(self.stateQueue, ^{ if (self.appInBackground) { self.shouldReconnectOnForeground = YES; return; } self.keepConnection = YES; self.pendingStart = NO; self.reconnectAttempts = 0; if (self.apiKey.length == 0) { NSLog(@"[DeepgramStreamingManager] Prepare skipped: API key missing"); return; } if (self.client.isConnected) { return; } [self connectIfNeeded]; }); } - (void)stopAndFinalize { dispatch_async(self.stateQueue, ^{ if (self.streaming) { [self.audioCapture stopCapture]; self.streaming = NO; } [self.pendingFrames removeAllObjects]; self.pendingStart = NO; if (self.client.isConnected) { [self.client finish]; } [self.client disableAudioSending]; [self startKeepAliveIfNeeded]; }); } - (void)cancel { dispatch_async(self.stateQueue, ^{ if (self.streaming) { [self.audioCapture stopCapture]; self.streaming = NO; } [self.pendingFrames removeAllObjects]; self.pendingStart = NO; self.keepConnection = NO; [self.client disableAudioSending]; [self stopKeepAlive]; [self.client disconnect]; }); } - (void)disconnect { dispatch_async(self.stateQueue, ^{ [self disconnectInternal]; }); } - (void)disconnectInternal { if (self.streaming) { [self.audioCapture stopCapture]; self.streaming = NO; } [self.pendingFrames removeAllObjects]; self.pendingStart = NO; self.keepConnection = NO; self.shouldReconnectOnForeground = NO; [self.client disableAudioSending]; [self stopKeepAlive]; [self.client disconnect]; [self.audioSession deactivateSession]; } #pragma mark - AudioCaptureManagerDelegate - (void)audioCaptureManagerDidOutputPCMFrame:(NSData *)pcmFrame { if (pcmFrame.length == 0) { return; } dispatch_async(self.stateQueue, ^{ if (!self.streaming || !self.client.isConnected) { [self.pendingFrames addObject:pcmFrame]; if (self.pendingFrames.count > self.pendingFrameLimit) { [self.pendingFrames removeObjectAtIndex:0]; } return; } [self.client sendAudioPCMFrame:pcmFrame]; }); } - (void)audioCaptureManagerDidUpdateRMS:(float)rms { dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidUpdateRMS:)]) { [self.delegate deepgramStreamingManagerDidUpdateRMS:rms]; } }); } #pragma mark - AudioSessionManagerDelegate - (void)audioSessionManagerDidInterrupt:(KBAudioSessionInterruptionType)type { if (type == KBAudioSessionInterruptionTypeBegan) { [self cancel]; } } - (void)audioSessionManagerMicrophonePermissionDenied { [self reportErrorWithMessage:@"Microphone permission denied"]; } #pragma mark - DeepgramWebSocketClientDelegate - (void)deepgramClientDidConnect { dispatch_async(self.stateQueue, ^{ self.connecting = NO; self.reconnectAttempts = 0; self.reconnectScheduled = NO; [self beginStreamingIfReady]; [self startKeepAliveIfNeeded]; dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidConnect)]) { [self.delegate deepgramStreamingManagerDidConnect]; } }); }); } - (void)deepgramClientDidDisconnect:(NSError *_Nullable)error { dispatch_async(self.stateQueue, ^{ if (self.streaming) { [self.audioCapture stopCapture]; self.streaming = NO; } self.connecting = NO; [self.audioSession deactivateSession]; [self stopKeepAlive]; if (self.pendingStart || self.keepConnection) { [self scheduleReconnectWithError:error]; } }); dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidDisconnect:)]) { [self.delegate deepgramStreamingManagerDidDisconnect:error]; } }); } - (void)deepgramClientDidReceiveInterimTranscript:(NSString *)text { dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidReceiveInterimTranscript:)]) { [self.delegate deepgramStreamingManagerDidReceiveInterimTranscript:text]; } }); } - (void)deepgramClientDidReceiveFinalTranscript:(NSString *)text { dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidReceiveFinalTranscript:)]) { [self.delegate deepgramStreamingManagerDidReceiveFinalTranscript:text]; } }); } - (void)deepgramClientDidFail:(NSError *)error { [self reportError:error]; } #pragma mark - Error Reporting - (void)reportError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector (deepgramStreamingManagerDidFail:)]) { [self.delegate deepgramStreamingManagerDidFail:error]; } }); } - (void)reportErrorWithMessage:(NSString *)message { NSError *error = [NSError errorWithDomain:kDeepgramStreamingManagerErrorDomain code:-1 userInfo:@{ NSLocalizedDescriptionKey : message ?: @"" }]; [self reportError:error]; } - (void)connectIfNeeded { if (self.connecting || self.client.isConnected) { return; } if (self.serverURL.length == 0) { [self reportErrorWithMessage:@"Deepgram server URL is required"]; return; } self.client.serverURL = self.serverURL; self.client.apiKey = self.apiKey; self.client.language = self.language; self.client.model = self.model; self.client.punctuate = self.punctuate; self.client.smartFormat = self.smartFormat; self.client.interimResults = self.interimResults; self.client.encoding = self.encoding; self.client.sampleRate = self.sampleRate; self.client.channels = self.channels; [self.client disableAudioSending]; self.connecting = YES; [self.client connect]; } - (void)beginStreamingIfReady { if (!self.pendingStart) { return; } self.streaming = YES; [self.client enableAudioSending]; [self stopKeepAlive]; if (self.pendingFrames.count > 0) { NSArray *frames = [self.pendingFrames copy]; [self.pendingFrames removeAllObjects]; for (NSData *frame in frames) { [self.client sendAudioPCMFrame:frame]; } NSLog(@"[DeepgramStreamingManager] Flushed %lu pending frames", (unsigned long)frames.count); } } - (void)scheduleReconnectWithError:(NSError *_Nullable)error { if (self.reconnectScheduled || self.connecting || self.client.isConnected) { return; } if (self.appInBackground) { self.shouldReconnectOnForeground = YES; return; } if (self.reconnectAttempts >= self.maxReconnectAttempts) { NSLog(@"[DeepgramStreamingManager] Reconnect failed %ld times, stop retry. %@", (long)self.maxReconnectAttempts, error.localizedDescription ?: @""); self.pendingStart = NO; self.keepConnection = NO; return; } self.reconnectAttempts += 1; self.reconnectScheduled = YES; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), self.stateQueue, ^{ self.reconnectScheduled = NO; if (self.appInBackground) { self.shouldReconnectOnForeground = YES; return; } if (!self.pendingStart && !self.keepConnection) { return; } [self connectIfNeeded]; }); } - (void)setupNotifications { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(handleAppDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; [center addObserver:self selector:@selector(handleAppWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; } - (void)removeNotifications { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)handleAppDidEnterBackground { dispatch_async(self.stateQueue, ^{ self.appInBackground = YES; self.shouldReconnectOnForeground = self.keepConnection || self.pendingStart; self.pendingStart = NO; self.keepConnection = NO; if (self.streaming) { [self.audioCapture stopCapture]; self.streaming = NO; } [self.pendingFrames removeAllObjects]; [self.client disableAudioSending]; [self stopKeepAlive]; [self.client disconnect]; [self.audioSession deactivateSession]; NSLog(@"[DeepgramStreamingManager] App entered background, socket closed"); }); } - (void)handleAppWillEnterForeground { dispatch_async(self.stateQueue, ^{ self.appInBackground = NO; if (self.shouldReconnectOnForeground) { self.keepConnection = YES; self.reconnectAttempts = 0; [self connectIfNeeded]; } self.shouldReconnectOnForeground = NO; }); } - (void)startKeepAliveIfNeeded { if (!self.keepConnection || !self.client.isConnected || self.streaming) { return; } if (self.keepAliveTimer) { return; } self.keepAliveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.stateQueue); dispatch_source_set_timer(self.keepAliveTimer, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), 15 * NSEC_PER_SEC, 1 * NSEC_PER_SEC); __weak typeof(self) weakSelf = self; dispatch_source_set_event_handler(self.keepAliveTimer, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } [strongSelf.client sendKeepAlive]; }); dispatch_resume(self.keepAliveTimer); } - (void)stopKeepAlive { if (self.keepAliveTimer) { dispatch_source_cancel(self.keepAliveTimer); self.keepAliveTimer = nil; } } @end