Untitled

 avatar
unknown
plain_text
a month ago
6.0 kB
3
Indexable
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import notifee from '@notifee/react-native';

export type IOSInterruptionType = 
  | 'phone_call'
  | 'facetime'
  | 'siri'
  | 'system_alert'
  | 'other_app'
  | 'system_pressure'
  | 'media_playback'
  | 'unknown';

export type IOSInterruptionEvent = {
  type: 'began' | 'ended';
  reason: IOSInterruptionType;
  options?: {
    shouldResume?: boolean;
    wasSuspended?: boolean;
  };
};

class IOSAudioSessionManager {
  private listeners: Set<(event: IOSInterruptionEvent) => void> = new Set();
  private audioSessionEmitter?: NativeEventEmitter;
  private memoryWarningEmitter?: NativeEventEmitter;

  constructor() {
    if (Platform.OS === 'ios') {
      this.setupAudioSession();
      this.setupInterruptionListeners();
      this.setupMemoryWarningListener();
    }
  }

  private async setupAudioSession() {
    try {
      const { RNAudioRecorderPlayer } = NativeModules;
      await RNAudioRecorderPlayer.setAudioSession({
        category: 'playAndRecord',
        options: [
          'allowBluetooth',
          'allowBluetoothA2DP',
          'mixWithOthers',
          'defaultToSpeaker'
        ],
        mode: 'spokenAudio',
        categoryOptions: {
          mixWithOthers: false,
          duckOthers: true,
          interruptSpokenAudioAndMixWithOthers: false,
          allowBluetooth: true,
          allowBluetoothA2DP: true,
          allowAirPlay: true,
          defaultToSpeaker: true,
        }
      });
    } catch (error) {
      console.error('Failed to configure audio session:', error);
    }
  }

  private setupInterruptionListeners() {
    const { RNAudioRecorderPlayer } = NativeModules;
    this.audioSessionEmitter = new NativeEventEmitter(RNAudioRecorderPlayer);

    // Audio Session Interruptions
    this.audioSessionEmitter.addListener(
      'audioSessionInterruption',
      this.handleAudioInterruption
    );

    // Route changes (e.g., audio output changes)
    this.audioSessionEmitter.addListener(
      'audioRouteChange',
      this.handleAudioRouteChange
    );

    // Media Services were reset
    this.audioSessionEmitter.addListener(
      'mediaServicesWereLost',
      this.handleMediaServicesReset
    );

    this.audioSessionEmitter.addListener(
      'mediaServicesWereReset',
      this.handleMediaServicesReset
    );
  }

  private setupMemoryWarningListener() {
    // System memory warnings
    this.memoryWarningEmitter = new NativeEventEmitter(NativeModules.RNEventEmitter);
    this.memoryWarningEmitter.addListener('memoryWarning', this.handleMemoryWarning);
  }

  private handleAudioInterruption = (event: any) => {
    const interruptionType = this.getInterruptionType(event);
    
    this.notifyListeners({
      type: event.type === 'began' ? 'began' : 'ended',
      reason: interruptionType,
      options: {
        shouldResume: event.type === 'ended' && event.options?.shouldResume,
        wasSuspended: event.wasSuspended
      }
    });

    this.updateNotification(event.type, interruptionType);
  };

  private handleAudioRouteChange = (event: any) => {
    // Handle audio route changes (e.g., between speaker, bluetooth)
    this.notifyListeners({
      type: 'began',
      reason: 'other_app',
      options: { shouldResume: true }
    });
  };

  private handleMediaServicesReset = () => {
    // Handle media service resets (rare but can happen)
    this.notifyListeners({
      type: 'began',
      reason: 'system_pressure',
      options: { shouldResume: true }
    });
  };

  private handleMemoryWarning = () => {
    this.notifyListeners({
      type: 'began',
      reason: 'system_pressure',
      options: { shouldResume: false }
    });
  };

  private getInterruptionType(event: any): IOSInterruptionType {
    // iOS provides this in AVAudioSession interruption notifications
    switch (event.reason) {
      case 'AVAudioSessionInterruptionTypePhoneCall':
        return 'phone_call';
      case 'AVAudioSessionInterruptionTypeFaceTime':
        return 'facetime';
      case 'AVAudioSessionInterruptionTypeSiri':
        return 'siri';
      case 'AVAudioSessionInterruptionTypeSystemAlert':
        return 'system_alert';
      case 'AVAudioSessionInterruptionTypeMediaPlayback':
        return 'media_playback';
      default:
        return 'unknown';
    }
  }

  private async updateNotification(
    state: 'began' | 'ended',
    type: IOSInterruptionType
  ) {
    const messages = {
      phone_call: 'Phone Call',
      facetime: 'FaceTime Call',
      siri: 'Siri',
      system_alert: 'System Alert',
      other_app: 'Other App Audio',
      system_pressure: 'System Resources',
      media_playback: 'Media Playback',
      unknown: 'System Interruption'
    };

    await notifee.displayNotification({
      id: 'recording-notification',
      title: state === 'began' ? 'Recording Paused' : 'Recording Resumed',
      body: state === 'began' 
        ? `Recording paused due to ${messages[type]}`
        : 'Recording has resumed',
      ios: {
        categoryId: 'recording',
        interruptionLevel: 'active'
      }
    });
  }

  public addListener(listener: (event: IOSInterruptionEvent) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notifyListeners(event: IOSInterruptionEvent): void {
    this.listeners.forEach(listener => listener(event));
  }

  public cleanup(): void {
    if (this.audioSessionEmitter) {
      this.audioSessionEmitter.removeAllListeners('audioSessionInterruption');
      this.audioSessionEmitter.removeAllListeners('audioRouteChange');
      this.audioSessionEmitter.removeAllListeners('mediaServicesWereLost');
      this.audioSessionEmitter.removeAllListeners('mediaServicesWereReset');
    }
    if (this.memoryWarningEmitter) {
      this.memoryWarningEmitter.removeAllListeners('memoryWarning');
    }
    this.listeners.clear();
  }
}

export const iOSAudioManager = new IOSAudioSessionManager();
Leave a Comment