This commit is contained in:
2026-01-22 13:47:34 +08:00
parent 06a572c08a
commit edc25c159d
10 changed files with 2308 additions and 7 deletions

View File

@@ -0,0 +1,508 @@
//
// DeepgramStreamingManager.m
// keyBoard
//
// Created by Mac on 2026/1/21.
//
#import "DeepgramStreamingManager.h"
#import "AudioCaptureManager.h"
#import "AudioSessionManager.h"
#import "DeepgramWebSocketClient.h"
#import <UIKit/UIKit.h>
static NSString *const kDeepgramStreamingManagerErrorDomain =
@"DeepgramStreamingManager";
@interface DeepgramStreamingManager () <AudioSessionManagerDelegate,
AudioCaptureManagerDelegate,
DeepgramWebSocketClientDelegate>
@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<NSData *> *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 disconnect];
}
- (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;
[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, ^{
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<NSData *> *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