如何在 iOS 上录制对话/电话?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1809347/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 16:39:46  来源:igfitidea点击:

How can I record a conversation / phone call on iOS?

iosiphoneaudioaudio-recording

提问by Adam Dempsey

Is it theoretically possible to record a phone call on iPhone?

理论上可以在 iPhone 上录制电话吗?

I'm accepting answers which:

我接受以下答案:

  • may or may not require the phone to be jailbroken
  • may or may not pass apple's guidelines due to use of private API's (I don't care; it is not for the App Store)
  • may or may not use private SDKs
  • 可能会或可能不会要求手机越狱
  • 由于使用私有 API,可能会或可能不会通过苹果的指南(我不在乎;它不适用于 App Store)
  • 可能会也可能不会使用私有 SDK

I don't want answers just bluntly saying "Apple does not allow that". I know there would be no official way of doing it, and certainly not for an App Store application, and I know there are call recording apps which place outgoing calls through their own servers.

我不想回答只是直截了当地说“Apple 不允许那样做”。我知道没有官方的方法可以做到这一点,当然也不是 App Store 应用程序,而且我知道有一些通话录音应用程序可以通过自己的服务器拨打电话。

回答by creker

Here you go. Complete working example. Tweak should be loaded in mediaserverddaemon. It will record every phone call in /var/mobile/Media/DCIM/result.m4a. Audio file has two channels. Left is microphone, right is speaker. On iPhone 4S call is recorded only when the speaker is turned on. On iPhone 5, 5C and 5S call is recorded either way. There might be small hiccups when switching to/from speaker but recording will continue.

干得好。完整的工作示例。调整应该在mediaserverd守护进程中加载。它将记录每个电话/var/mobile/Media/DCIM/result.m4a。音频文件有两个通道。左边是麦克风,右边是扬声器。iPhone 4S 通话仅在扬声器打开时录音。在 iPhone 5 上,5C 和 5S 的通话都是以任何一种方式录制的。切换到/从扬声器时可能会出现小问题,但录音会继续。

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

A few words about what's going on. AudioUnitProcessfunction is used for processing audio streams in order to apply some effects, mix, convert etc. We are hooking AudioUnitProcessin order to access phone call's audio streams. While phone call is active these streams are being processed in various ways.

关于正在发生的事情的几句话。AudioUnitProcess函数用于处理音频流以应用一些效果、混合、转换等。我们正在挂钩AudioUnitProcess以访问电话的音频流。当电话处于活动状态时,这些流正在以各种方式进行处理。

We are listening for CoreTelephony notifications in order to get phone call status changes. When we receive audio samples we need to determine where they come from - microphone or speaker. This is done using componentSubTypefield in AudioComponentDescriptionstructure. Now, you might think, why don't we store AudioUnitobjects so that we don't need to check componentSubTypeevery time. I did that but it will break everything when you switch speaker on/off on iPhone 5 because AudioUnitobjects will change, they are recreated. So, now we open audio files (one for microphone and one for speaker) and write samples in them, simple as that. When phone call ends we will receive appropriate CoreTelephony notification and close the files. We have two separate files with audio from microphone and speaker that we need to merge. This is what void Convert()is for. It's pretty simple if you know the API. I don't think I need to explain it, comments are enough.

我们正在侦听 CoreTelephony 通知以获取电话状态更改。当我们收到音频样本时,我们需要确定它们来自哪里——麦克风或扬声器。这是使用结构中的componentSubType字段完成的AudioComponentDescription。现在,您可能会想,为什么我们不存储AudioUnit对象,这样我们就不需要componentSubType每次都检查。我这样做了,但是当你在 iPhone 5 上打开/关闭扬声器时它会破坏一切,因为AudioUnit对象会改变,它们会被重新创建。所以,现在我们打开音频文件(一个用于麦克风,一个用于扬声器)并在其中写入样本,就这么简单。当电话结束时,我们将收到相应的 CoreTelephony 通知并关闭文件。我们有两个单独的文件,其中包含需要合并的麦克风和扬声器的音频。这是什么void Convert()是为了。如果您了解 API,这将非常简单。我想我不需要解释它,评论就足够了。

About locks. There are many threads in mediaserverd. Audio processing and CoreTelephony notifications are on different threads so we need some kind synchronization. I chose spin locks because they are fast and because the chance of lock contention is small in our case. On iPhone 4S and even iPhone 5 all the work in AudioUnitProcessshould be done as fast as possible otherwise you will hear hiccups from device speaker which obviously not good.

关于锁。中有很多线程mediaserverd。音频处理和 CoreTelephony 通知在不同的线程上,所以我们需要某种同步。我选择自旋锁是因为它们速度快,而且在我们的例子中锁争用的可能性很小。在 iPhone 4S 甚至 iPhone 5 上,所有工作都AudioUnitProcess应该尽快完成,否则你会听到设备扬声器发出的打嗝声,这显然不好。

回答by Segev

Yes. Audio Recorderby a developer named Limneos does that (and quite well). You can find it on Cydia. It can record any type of call on iPhone 5 and up without using any servers etc'. The call will be placed on the device in an Audio file. It also supports iPhone 4S but for speaker only.

是的。一位名叫 Limneos 的开发人员的Audio Recorder做到了(而且相当不错)。你可以在 Cydia 上找到它。它可以在不使用任何服务器等的情况下记录 iPhone 5 及更高版本上的任何类型的通话。呼叫将放置在设备上的音频文件中。它还支持 iPhone 4S,但仅适用于扬声器。

This tweak is known to be the first tweak ever that managed to record both streams of audio without using any 3rd party severs, VOIP or something similar.

众所周知,这种调整是有史以来第一次成功地在不使用任何 3rd 方服务器、VOIP 或类似设备的情况下记录两个音频流的调整。

The developer placed beeps on the other side of the call to alert the person you are recording but those were removed too by hackers across the net. To answer your question, Yes, it's very much possible, and not just theoretically.

开发人员在通话的另一端发出哔哔声以提醒您正在录音的人,但这些声音也被网络上的黑客删除了。要回答您的问题,是的,这是很有可能的,而不仅仅是理论上。

enter image description here

在此处输入图片说明

Further reading

进一步阅读

回答by Samuel Noyes

The only solution I can think of is to use the Core Telephonyframework, and more specifically the callEventHandlerproperty, to intercept when a call is coming in, and then to use an AVAudioRecorderto record the voice of the person with the phone (and maybe a little of the person on the other line's voice). This is obviously not perfect, and would only work if your application is in the foreground at the time of the call, but it may be the best you can get. See more about finding out if there is an incoming phone call here: Can we fire an event when ever there is Incoming and Outgoing call in iphone?.

我能想到的唯一解决方案是使用Core Telephony框架,更具体地说是callEventHandler属性,在来电时进行拦截,然后使用AVAudioRecorder用电话记录人的声音(也许另一条线路上的人的一点声音)。这显然不是完美的,并且只有当您的应用程序在调用时处于前台时才有效,但它可能是您可以获得的最好的。在此处查看有关了解是否有来电的更多信息:当 iphone 中有来电和去电时,我们可以触发事件吗?.

EDIT:

编辑:

.h:

。H:

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

ViewDidLoad:

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:

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

This is the first time using many of these features, so not sure if this is exactly right, but I think you get the idea. Untested, as I do not have access to the right tools at the moment. Compiled using these sources:

这是第一次使用其中的许多功能,所以不确定这是否完全正确,但我想你明白了。未经测试,因为我目前无法使用正确的工具。使用这些来源编译:

回答by Dimitris

Apple does not allow it and does not provide any API for it.

Apple 不允许,也没有为其提供任何 API。

However, on a jailbroken device I'm sure it's possible. As a matter of fact, I think it's already done. I remember seeing an app when my phone was jailbroken that changed your voice and recorded the call - I remember it was a US company offering it only in the states. Unfortunately I don't remember the name...

但是,在越狱设备上,我确信这是可能的。事实上,我认为它已经完成了。我记得在我的手机越狱时看到一个应用程序可以改变你的声音并记录通话 - 我记得它是一家美国公司,仅在美国提供。可惜我不记得名字了...

回答by hfossli

I guess some hardware could solve this. Connected to the miniHyman-port; having earbuds and a microphone passing through a small recorder. This recorder can be very simple. While not in conversation the recorder could feed the phone with data/the recording (through the Hyman-cable). And with a simple start button (just like the volum controls on the earbuds) could be sufficient for timing the recording.

我想一些硬件可以解决这个问题。连接到 miniHyman 端口;有耳塞和麦克风穿过小型录音机。这个记录器可以非常简单。虽然不在通话中,录音机可以向电话提供数据/录音(通过插孔电缆)。使用一个简单的开始按钮(就像耳塞上的音量控制)就足以为录音计时。

Some setups

一些设置