web-dev-qa-db-fra.com

Comment enregistrer une conversation / un appel téléphonique sur iOS?

Est-il théoriquement possible d'enregistrer un appel téléphonique sur iPhone?

J'accepte des réponses qui:

  • peut ou non exiger que le téléphone soit jailbreaké
  • peut ou peut ne pas respecter les directives d'Apple en raison de l'utilisation d'API privées (je m'en fous; ce n'est pas pour l'App Store)
  • peut ou non utiliser des SDK privés

Je ne veux pas de réponses disant simplement "Apple ne le permet pas". Je sais qu'il n'y aurait aucun moyen officiel de le faire, et certainement pas pour une application App Store, et je sais qu'il existe des applications d'enregistrement d'appels qui passent des appels sortants via leurs propres serveurs.

55
Adam Dempsey

Voici. Exemple de travail complet. Tweak doit être chargé dans le démon mediaserverd. Il enregistrera chaque appel téléphonique dans /var/mobile/Media/DCIM/result.m4a. Le fichier audio a deux canaux. Gauche est le microphone, droite est le haut-parleur. Sur l'iPhone 4S, l'appel n'est enregistré que lorsque le haut-parleur est allumé. Sur iPhone 5, 5C et 5S, les appels sont enregistrés dans les deux sens. Il peut y avoir de petits hoquets lors du passage de/vers le haut-parleur, mais l'enregistrement continuera.

#import <AudioToolbox/AudioToolbox.h>
#import <libkern/OSAtomic.h>

//CoreTelephony.framework
extern "C" CFStringRef const kCTCallStatusChangeNotification;
extern "C" CFStringRef const kCTCallStatus;
extern "C" id CTTelephonyCenterGetDefault();
extern "C" void CTTelephonyCenterAddObserver(id ct, void* observer, CFNotificationCallback callBack, CFStringRef name, void *object, CFNotificationSuspensionBehavior sb);
extern "C" int CTGetCurrentCallCount();
enum
{
    kCTCallStatusActive = 1,
    kCTCallStatusHeld = 2,
    kCTCallStatusOutgoing = 3,
    kCTCallStatusIncoming = 4,
    kCTCallStatusHanged = 5
};

NSString* kMicFilePath = @"/var/mobile/Media/DCIM/mic.caf";
NSString* kSpeakerFilePath = @"/var/mobile/Media/DCIM/speaker.caf";
NSString* kResultFilePath = @"/var/mobile/Media/DCIM/result.m4a";

OSSpinLock phoneCallIsActiveLock = 0;
OSSpinLock speakerLock = 0;
OSSpinLock micLock = 0;

ExtAudioFileRef micFile = NULL;
ExtAudioFileRef speakerFile = NULL;

BOOL phoneCallIsActive = NO;

void Convert()
{
    //File URLs
    CFURLRef micUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kMicFilePath, kCFURLPOSIXPathStyle, false);
    CFURLRef speakerUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kSpeakerFilePath, kCFURLPOSIXPathStyle, false);
    CFURLRef mixUrl = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)kResultFilePath, kCFURLPOSIXPathStyle, false);

    ExtAudioFileRef micFile = NULL;
    ExtAudioFileRef speakerFile = NULL;
    ExtAudioFileRef mixFile = NULL;

    //Opening input files (speaker and mic)
    ExtAudioFileOpenURL(micUrl, &micFile);
    ExtAudioFileOpenURL(speakerUrl, &speakerFile);

    //Reading input file audio format (mono LPCM)
    AudioStreamBasicDescription inputFormat, outputFormat;
    UInt32 descSize = sizeof(inputFormat);
    ExtAudioFileGetProperty(micFile, kExtAudioFileProperty_FileDataFormat, &descSize, &inputFormat);
    int sampleSize = inputFormat.mBytesPerFrame;

    //Filling input stream format for output file (stereo LPCM)
    FillOutASBDForLPCM(inputFormat, inputFormat.mSampleRate, 2, inputFormat.mBitsPerChannel, inputFormat.mBitsPerChannel, true, false, false);

    //Filling output file audio format (AAC)
    memset(&outputFormat, 0, sizeof(outputFormat));
    outputFormat.mFormatID = kAudioFormatMPEG4AAC;
    outputFormat.mSampleRate = 8000;
    outputFormat.mFormatFlags = kMPEG4Object_AAC_Main;
    outputFormat.mChannelsPerFrame = 2;

    //Opening output file
    ExtAudioFileCreateWithURL(mixUrl, kAudioFileM4AType, &outputFormat, NULL, kAudioFileFlags_EraseFile, &mixFile);
    ExtAudioFileSetProperty(mixFile, kExtAudioFileProperty_ClientDataFormat, sizeof(inputFormat), &inputFormat);

    //Freeing URLs
    CFRelease(micUrl);
    CFRelease(speakerUrl);
    CFRelease(mixUrl);

    //Setting up audio buffers
    int bufferSizeInSamples = 64 * 1024;

    AudioBufferList micBuffer;
    micBuffer.mNumberBuffers = 1;
    micBuffer.mBuffers[0].mNumberChannels = 1;
    micBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples;
    micBuffer.mBuffers[0].mData = malloc(micBuffer.mBuffers[0].mDataByteSize);

    AudioBufferList speakerBuffer;
    speakerBuffer.mNumberBuffers = 1;
    speakerBuffer.mBuffers[0].mNumberChannels = 1;
    speakerBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples;
    speakerBuffer.mBuffers[0].mData = malloc(speakerBuffer.mBuffers[0].mDataByteSize);

    AudioBufferList mixBuffer;
    mixBuffer.mNumberBuffers = 1;
    mixBuffer.mBuffers[0].mNumberChannels = 2;
    mixBuffer.mBuffers[0].mDataByteSize = sampleSize * bufferSizeInSamples * 2;
    mixBuffer.mBuffers[0].mData = malloc(mixBuffer.mBuffers[0].mDataByteSize);

    //Converting
    while (true)
    {
        //Reading data from input files
        UInt32 framesToRead = bufferSizeInSamples;
        ExtAudioFileRead(micFile, &framesToRead, &micBuffer);
        ExtAudioFileRead(speakerFile, &framesToRead, &speakerBuffer);
        if (framesToRead == 0)
        {
            break;
        }

        //Building interleaved stereo buffer - left channel is mic, right - speaker
        for (int i = 0; i < framesToRead; i++)
        {
            memcpy((char*)mixBuffer.mBuffers[0].mData + i * sampleSize * 2, (char*)micBuffer.mBuffers[0].mData + i * sampleSize, sampleSize);
            memcpy((char*)mixBuffer.mBuffers[0].mData + i * sampleSize * 2 + sampleSize, (char*)speakerBuffer.mBuffers[0].mData + i * sampleSize, sampleSize);
        }

        //Writing to output file - LPCM will be converted to AAC
        ExtAudioFileWrite(mixFile, framesToRead, &mixBuffer);
    }

    //Closing files
    ExtAudioFileDispose(micFile);
    ExtAudioFileDispose(speakerFile);
    ExtAudioFileDispose(mixFile);

    //Freeing audio buffers
    free(micBuffer.mBuffers[0].mData);
    free(speakerBuffer.mBuffers[0].mData);
    free(mixBuffer.mBuffers[0].mData);
}

void Cleanup()
{
    [[NSFileManager defaultManager] removeItemAtPath:kMicFilePath error:NULL];
    [[NSFileManager defaultManager] removeItemAtPath:kSpeakerFilePath error:NULL];
}

void CoreTelephonyNotificationCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
{
    NSDictionary* data = (NSDictionary*)userInfo;

    if ([(NSString*)name isEqualToString:(NSString*)kCTCallStatusChangeNotification])
    {
        int currentCallStatus = [data[(NSString*)kCTCallStatus] integerValue];

        if (currentCallStatus == kCTCallStatusActive)
        {
            OSSpinLockLock(&phoneCallIsActiveLock);
            phoneCallIsActive = YES;
            OSSpinLockUnlock(&phoneCallIsActiveLock);
        }
        else if (currentCallStatus == kCTCallStatusHanged)
        {
            if (CTGetCurrentCallCount() > 0)
            {
                return;
            }

            OSSpinLockLock(&phoneCallIsActiveLock);
            phoneCallIsActive = NO;
            OSSpinLockUnlock(&phoneCallIsActiveLock);

            //Closing mic file
            OSSpinLockLock(&micLock);
            if (micFile != NULL)
            {
                ExtAudioFileDispose(micFile);
            }
            micFile = NULL;
            OSSpinLockUnlock(&micLock);

            //Closing speaker file
            OSSpinLockLock(&speakerLock);
            if (speakerFile != NULL)
            {
                ExtAudioFileDispose(speakerFile);
            }
            speakerFile = NULL;
            OSSpinLockUnlock(&speakerLock);

            Convert();
            Cleanup();
        }
    }
}

OSStatus(*AudioUnitProcess_orig)(AudioUnit unit, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inNumberFrames, AudioBufferList *ioData);
OSStatus AudioUnitProcess_hook(AudioUnit unit, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inNumberFrames, AudioBufferList *ioData)
{
    OSSpinLockLock(&phoneCallIsActiveLock);
    if (phoneCallIsActive == NO)
    {
        OSSpinLockUnlock(&phoneCallIsActiveLock);
        return AudioUnitProcess_orig(unit, ioActionFlags, inTimeStamp, inNumberFrames, ioData);
    }
    OSSpinLockUnlock(&phoneCallIsActiveLock);

    ExtAudioFileRef* currentFile = NULL;
    OSSpinLock* currentLock = NULL;

    AudioComponentDescription unitDescription = {0};
    AudioComponentGetDescription(AudioComponentInstanceGetComponent(unit), &unitDescription);
    //'agcc', 'mbdp' - iPhone 4S, iPhone 5
    //'agc2', 'vrq2' - iPhone 5C, iPhone 5S
    if (unitDescription.componentSubType == 'agcc' || unitDescription.componentSubType == 'agc2')
    {
        currentFile = &micFile;
        currentLock = &micLock;
    }
    else if (unitDescription.componentSubType == 'mbdp' || unitDescription.componentSubType == 'vrq2')
    {
        currentFile = &speakerFile;
        currentLock = &speakerLock;
    }

    if (currentFile != NULL)
    {
        OSSpinLockLock(currentLock);

        //Opening file
        if (*currentFile == NULL)
        {
            //Obtaining input audio format
            AudioStreamBasicDescription desc;
            UInt32 descSize = sizeof(desc);
            AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &desc, &descSize);

            //Opening audio file
            CFURLRef url = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)((currentFile == &micFile) ? kMicFilePath : kSpeakerFilePath), kCFURLPOSIXPathStyle, false);
            ExtAudioFileRef audioFile = NULL;
            OSStatus result = ExtAudioFileCreateWithURL(url, kAudioFileCAFType, &desc, NULL, kAudioFileFlags_EraseFile, &audioFile);
            if (result != 0)
            {
                *currentFile = NULL;
            }
            else
            {
                *currentFile = audioFile;

                //Writing audio format
                ExtAudioFileSetProperty(*currentFile, kExtAudioFileProperty_ClientDataFormat, sizeof(desc), &desc);
            }
            CFRelease(url);
        }
        else
        {
            //Writing audio buffer
            ExtAudioFileWrite(*currentFile, inNumberFrames, ioData);
        }

        OSSpinLockUnlock(currentLock);
    }

    return AudioUnitProcess_orig(unit, ioActionFlags, inTimeStamp, inNumberFrames, ioData);
}

__attribute__((constructor))
static void initialize()
{
    CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), NULL, CoreTelephonyNotificationCallback, NULL, NULL, CFNotificationSuspensionBehaviorHold);

    MSHookFunction(AudioUnitProcess, AudioUnitProcess_hook, &AudioUnitProcess_orig);
}

Quelques mots sur ce qui se passe. La fonction AudioUnitProcess est utilisée pour traiter les flux audio afin d'appliquer certains effets, mélanger, convertir, etc. Nous accrochons AudioUnitProcess afin d'accéder aux flux audio des appels téléphoniques. Pendant que l'appel téléphonique est actif, ces flux sont traités de différentes manières.

Nous écoutons les notifications CoreTelephony afin d'obtenir des changements de statut des appels téléphoniques. Lorsque nous recevons des échantillons audio, nous devons déterminer d'où ils proviennent - microphone ou haut-parleur. Cela se fait en utilisant le champ componentSubType dans la structure AudioComponentDescription. Maintenant, vous pourriez penser, pourquoi ne stockons-nous pas les objets AudioUnit afin que nous n'ayons pas besoin de vérifier componentSubType à chaque fois. Je l'ai fait mais cela cassera tout lorsque vous allumez/éteignez le haut-parleur sur l'iPhone 5 car les objets AudioUnit vont changer, ils sont recréés. Donc, maintenant nous ouvrons des fichiers audio (un pour le microphone et un pour le haut-parleur) et y écrivons des échantillons, aussi simple que cela. À la fin de l'appel téléphonique, nous recevrons une notification CoreTelephony appropriée et fermerons les fichiers. Nous avons deux fichiers distincts avec l'audio du microphone et du haut-parleur que nous devons fusionner. C'est à cela que sert void Convert(). C'est assez simple si vous connaissez l'API. Je ne pense pas avoir besoin de l'expliquer, les commentaires suffisent.

À propos des verrous. Il existe de nombreux threads dans mediaserverd. Le traitement audio et les notifications CoreTelephony sont sur des threads différents, nous avons donc besoin d'une sorte de synchronisation. J'ai choisi les verrous tournants parce qu'ils sont rapides et parce que le risque de contention de verrouillage est faible dans notre cas. Sur l'iPhone 4S et même l'iPhone 5, tout le travail dans AudioUnitProcess devrait être fait aussi vite que possible, sinon vous entendrez des hoquets du haut-parleur de l'appareil, ce qui n'est évidemment pas bon.

72
creker

Oui. Audio Recorder par un développeur nommé Limneos fait cela (et très bien). Vous pouvez le trouver sur Cydia. Il peut enregistrer tout type d'appel sur iPhone 5 et plus sans utiliser de serveurs, etc. L'appel sera placé sur l'appareil dans un fichier audio. Il prend également en charge l'iPhone 4S mais uniquement pour les haut-parleurs.

Ce Tweak est connu pour être le premier Tweak à avoir réussi à enregistrer les deux flux audio sans utiliser de serveurs tiers, VOIP ou quelque chose de similaire.

Le développeur a placé des bips de l'autre côté de l'appel pour alerter la personne que vous enregistrez, mais ceux-ci ont également été supprimés par des pirates sur le net. Pour répondre à votre question, oui, c'est tout à fait possible, et pas seulement théorique.

enter image description here

Lectures complémentaires

9
Segev

La seule solution à laquelle je peux penser est d'utiliser le framework Core Telephony , et plus spécifiquement la propriété callEventHandler , pour intercepter lorsqu'un appel arrive, puis utiliser un AVAudioRecorder pour enregistrer la voix de la personne avec le téléphone (et peut-être un peu de la personne sur la voix de l'autre ligne). Ce n'est évidemment pas parfait et ne fonctionnerait que si votre application est au premier plan au moment de l'appel, mais c'est peut-être le meilleur que vous puissiez obtenir. Pour en savoir plus sur la détection d'un appel téléphonique, cliquez ici: Pouvons-nous déclencher un événement lorsqu'il y a un appel entrant et sortant sur l'iphone? .

MODIFIER:

.h:

#import <AVFoundation/AVFoundation.h>
#import<CoreTelephony/CTCallCenter.h>
#import<CoreTelephony/CTCall.h>
@property (strong, nonatomic) AVAudioRecorder *audioRecorder;

ViewDidLoad:

NSArray *dirPaths;
NSString *docsDir;

dirPaths = NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask, YES);
docsDir = dirPaths[0];

NSString *soundFilePath = [docsDir
   stringByAppendingPathComponent:@"sound.caf"];

NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];

NSDictionary *recordSettings = [NSDictionary
        dictionaryWithObjectsAndKeys:
        [NSNumber numberWithInt:AVAudioQualityMin],
        AVEncoderAudioQualityKey,
        [NSNumber numberWithInt:16],
        AVEncoderBitRateKey,
        [NSNumber numberWithInt: 2],
        AVNumberOfChannelsKey,
        [NSNumber numberWithFloat:44100.0],
        AVSampleRateKey,
        nil];

NSError *error = nil;

_audioRecorder = [[AVAudioRecorder alloc]
              initWithURL:soundFileURL
              settings:recordSettings
              error:&error];

 if (error)
 {
       NSLog(@"error: %@", [error localizedDescription]);
 } else {
       [_audioRecorder prepareToRecord];
 }

CTCallCenter *callCenter = [[CTCallCenter alloc] init];

[callCenter setCallEventHandler:^(CTCall *call) {
  if ([[call callState] isEqual:CTCallStateConnected]) {
    [_audioRecorder record];
  } else if ([[call callState] isEqual:CTCallStateDisconnected]) {
    [_audioRecorder stop];
  }
}];

AppDelegate.m:

- (void)applicationDidEnterBackground:(UIApplication *)application//Makes sure that the recording keeps happening even when app is in the background, though only can go for 10 minutes.
{
    __block UIBackgroundTaskIdentifier task = 0;
    task=[application beginBackgroundTaskWithExpirationHandler:^{
    NSLog(@"Expiration handler called %f",[application backgroundTimeRemaining]);
    [application endBackgroundTask:task];
    task=UIBackgroundTaskInvalid;
}];

C'est la première fois que vous utilisez plusieurs de ces fonctionnalités, donc vous ne savez pas si c'est exactement le cas, mais je pense que vous avez compris. Non testé, car je n'ai pas accès aux bons outils pour le moment. Compilé à l'aide de ces sources:

7
Samuel Noyes

Je suppose que certains matériels pourraient résoudre ce problème. Connecté au port minijack; ayant des écouteurs et un microphone passant à travers un petit enregistreur. Cet enregistreur peut être très simple. Lorsqu'il n'est pas en conversation, l'enregistreur peut alimenter le téléphone avec des données/l'enregistrement (via le câble jack). Et avec un simple bouton de démarrage (tout comme les commandes de volume sur les écouteurs) pourrait être suffisant pour chronométrer l'enregistrement.

Quelques configurations

2
hfossli

Apple ne le permet pas et ne fournit aucune API pour cela.

Cependant, sur un appareil jailbreaké, je suis sûr que c'est possible. En fait, je pense que c'est déjà fait. Je me souviens avoir vu une application lorsque mon téléphone a été jailbreaké qui a changé votre voix et enregistré l'appel - je me souviens que c'était une entreprise américaine qui ne la proposait qu'aux États-Unis. Malheureusement, je ne me souviens pas du nom ...

2
Dimitris