Question

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

J'accepte les réponses qui:

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

Je ne veux pas que les réponses disent simplement "Apple ne le permet pas". Je sais qu'il n'y aurait pas de 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.

Était-ce utile?

La solution

Ici vous allez. 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, le microphone, à droite, le haut-parleur. Sur l’iPhone 4S, l’appel n’est enregistré que lorsque le haut-parleur est activé. Sur les iPhone 5, les appels 5C et 5S sont enregistrés dans les deux sens. Il peut y avoir de petits problèmes lors du changement de haut-parleur mais l’enregistrement se poursuivra.

#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 le traitement des 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. Lorsqu'un appel téléphonique est actif, ces flux sont traités de différentes manières.

Nous écoutons les notifications CoreTelephony afin d’obtenir les changements de statut des appels téléphoniques. Lorsque nous recevons des échantillons audio, nous devons déterminer leur provenance - microphone ou haut-parleur. Pour ce faire, utilisez le champ composantSubType de la structure AudioComponentDescription . Maintenant, vous pourriez penser, pourquoi ne stockons-nous pas les objets AudioUnit afin de ne pas avoir besoin de vérifier composantSubType à chaque fois. C'est ce que j'ai fait, mais tout va casser lorsque vous allumez ou éteignez le haut-parleur sur l'iPhone 5, car les objets AudioUnit vont changer, ils sont recréés. Nous ouvrons maintenant des fichiers audio (un pour le microphone et un pour le haut-parleur) et y écrivons des échantillons, aussi simple que cela. Lorsque l'appel téléphonique se termine, nous recevons la notification appropriée de CoreTelephony et fermons les fichiers. Nous avons deux fichiers distincts avec l'audio du microphone et du haut-parleur que nous devons fusionner. C’est ce à quoi void Convert () est destiné. C'est assez simple si vous connaissez l'API. Je ne pense pas avoir besoin de l'expliquer, les commentaires suffisent.

À propos des verrous. mediaserverd contient de nombreux threads. 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 à rotation parce qu'ils sont rapides et que les risques de contention des verrous sont faibles dans notre cas. Sur l'iPhone 4S et même l'iPhone 5, tout le travail effectué dans AudioUnitProcess doit être effectué aussi rapidement que possible, sinon vous entendrez des hoquets provenant du haut-parleur de l'appareil, ce qui n'est évidemment pas satisfaisant.

Autres conseils

Oui. Enregistreur audio par un développeur nommé Limneos le fait (et très bien). Vous pouvez le trouver sur Cydia. Il peut enregistrer n’importe quel type d’appel sur iPhone 5 ou supérieur sans passer par aucun serveur, 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 à avoir jamais réussi à enregistrer les deux flux audio sans utiliser de tiers, de VOIP ou quelque chose de similaire.

Le développeur a placé des signaux sonores 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 informatiques du réseau. Pour répondre à votre question, oui, c’est tout à fait possible, et pas seulement en théorie.

entrer la description de l

Lectures supplémentaires

La seule solution à laquelle je puisse penser consiste à utiliser Core Telephony , et plus particulièrement le callEventHandler , pour intercepter l’appel entrant, puis utiliser une 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 choix que vous puissiez obtenir. Pour en savoir plus sur le fait de savoir s’il ya un appel téléphonique entrant, cliquez ici: Peut-on déclencher un événement chaque fois qu'il y a un appel entrant ou sortant dans un iphone? .

EDIT:

.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 l’on utilise beaucoup de ces fonctionnalités, donc je ne sais pas si c’est exactement ça, 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:

Apple ne le permet pas et ne fournit aucune API à ce sujet.

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 était jailbreaké, ce qui changea votre voix et enregistra l'appel. Je me souviens que c'était une société américaine qui ne la proposait que dans les États. Malheureusement, je ne me souviens plus du nom ...

Je suppose que certains matériels pourraient résoudre ce problème. Connecté au minijack-port; avoir des écouteurs et un microphone dans 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 en données / enregistrement (via le câble jack). Et avec un simple bouton de démarrage (tout comme les commandes de volume sur les oreillettes) pourrait être suffisant pour chronométrer l'enregistrement.

Certaines configurations

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top