¿Cómo puedo grabar una conversación / llamada telefónica en iOS?

¿Teóricamente es posible grabar una llamada telefónica en el iPhone?

Estoy aceptando respuestas que:

  • puede o no requerir que el teléfono sea jailbreak
  • puede o no pasar las pautas de Apple debido al uso de API privadas (no me importa, no es para la App Store)
  • puede o no usar SDK privados

No quiero respuestas simplemente diciendo "Apple no lo permite". Sé que no habría forma oficial de hacerlo, y ciertamente no para una aplicación App Store, y sé que hay aplicaciones de grabación de llamadas que colocan llamadas salientes a través de sus propios serveres.

Aqui tienes. Ejemplo de trabajo completo. El ajuste debe cargarse en el daemon de mediaserverd . /var/mobile/Media/DCIM/result.m4a todas las llamadas telefónicas en /var/mobile/Media/DCIM/result.m4a . El file de audio tiene dos canales. Izquierda es micrófono, derecha es altavoz. En iPhone 4S, la llamada se graba solo cuando el altavoz está encendido. En iPhone 5, la llamada 5C y 5S se registra de cualquier manera. Puede haber pequeños contratimes al cambiar a / desde el altavoz, pero la grabación continuará.

 #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); } 

Unas pocas palabras sobre lo que está pasando. AudioUnitProcess function AudioUnitProcess se utiliza para procesar flujos de audio para aplicar algunos efectos, mezclar, convertir, etc. Estamos conectando AudioUnitProcess para acceder a las transmisiones de audio de la llamada telefónica. Mientras la llamada telefónica está activa, estas transmisiones se procesan de varias maneras.

Estamos escuchando las notifications de CoreTelephony para get cambios en el estado de las llamadas telefónicas. Cuando recibimos muestras de audio, debemos determinar de dónde vienen: micrófono o altavoz. Esto se hace utilizando el campo componentSubType en la estructura AudioComponentDescription . Ahora, podría pensar, ¿por qué no almacenamos objects AudioUnit para que no tengamos que verificar componentSubType cada vez. Lo hice pero romperá todo cuando active / desactive el altavoz en el iPhone 5 porque los objects AudioUnit cambiarán, se volverán a crear. Entonces, ahora abrimos files de audio (uno para micrófono y uno para altavoz) y escribimos muestras en ellos, simple como eso. Cuando termine la llamada, recibiremos la notificación apropiada de CoreTelephony y cerraremos los files. Tenemos dos files separados con audio de micrófono y altavoz que debemos fusionar. Esto es para lo que void Convert() . Es bastante simple si conoces la API. No creo que necesite explicarlo, los comentarios son suficientes.

Acerca de los lockings. Hay muchos subprocesss en mediaserverd . Las notifications de procesamiento de audio y CoreTelephony están en diferentes subprocesss, por lo que necesitamos alguna synchronization amable. Elegí cerraduras giratorias porque son rápidas y porque la posibilidad de contención de cerrojos es pequeña en nuestro caso. En iPhone 4S e incluso iPhone 5, todo el trabajo en AudioUnitProcess debe hacer lo más rápido posible, de lo contrario, se escuchará hipo del altavoz del dispositivo que obviamente no es bueno.

Sí. Audio Recorder de un desarrollador llamado Limneos hace eso (y bastante bien). Puedes encontrarlo en Cydia. Puede grabar cualquier tipo de llamada en el iPhone 5 en adelante sin usar ningún server, etc. La llamada se colocará en el dispositivo en un file de audio. También es compatible con iPhone 4S, pero solo para altavoces.

Este tweak es conocido por ser el primer tweak que logró grabar las dos secuencias de audio sin utilizar ninguna tercera parte, VOIP o algo similar.

El desarrollador colocó pitidos en el otro lado de la llamada para alertar a la persona que está grabando, pero también los eliminaron los piratas informáticos a través de la networking. Para responder a su pregunta, sí, es muy posible, y no solo teóricamente.

introduzca la descripción de la imagen aquí

Otras lecturas

La única solución que se me ocurre es utilizar el marco de Core Telefonía , y más específicamente la propiedad callEventHandler , para interceptar cuando se recibe una llamada, y luego usar un AVAudioRecorder para grabar la voz de la persona con el teléfono (y tal vez un poco de la persona en la voz de la otra línea). Obviamente, esto no es perfecto, y solo funcionaría si su aplicación está en primer plano en el momento de la llamada, pero puede ser lo mejor que pueda get. Vea más sobre averiguar si hay una llamada entrante aquí: ¿Podemos disparar un evento cuando alguna vez hay una llamada entrante y saliente en el iPhone? .

EDITAR:

.marido:

 #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; }]; 

Esta es la primera vez que usa muchas de estas características, por lo que no estoy seguro si esto es exactamente correcto, pero creo que obtienes la idea. No probado, ya que no tengo acceso a las herramientas correctas en este momento. Comstackdo usando estas fonts:

Supongo que algún hardware podría resolver esto. Conectado al minijack-port; Tener auriculares y un micrófono que pasa por una pequeña grabadora. Esta grabadora puede ser muy simple. Mientras no esté en conversación, la grabadora podría alimentar el teléfono con datos / la grabación (a través del jack-cable). Y con un simple button de inicio (al igual que los controles volum en los auriculares) podría ser suficiente para sincronizar la grabación.

Algunas configuraciones

Apple no lo permite y no proporciona ninguna API para ello.

Sin embargo, en un dispositivo Jailbroken, estoy seguro de que es posible. De hecho, creo que ya está hecho. Recuerdo haber visto una aplicación cuando mi teléfono estaba jailbroken, que cambió tu voz y grabó la llamada. Recuerdo que era una compañía estadounidense que solo la ofrecía en los Estados Unidos. Desafortunadamente no recuerdo el nombre …