문제

iPhone에서 전화를 녹음하는 것이 이론적으로 가능합니까?

나는 다음을 수락하고있다 :

  • 전화가 탈옥되도록 요구하지 않을 수도 있습니다.
  • 개인 API의 사용으로 인해 Apple의 지침을 통과하거나 전달하지 못할 수도 있습니다 (나는 신경 쓰지 않습니다. 그것은 App Store를위한 것이 아닙니다)
  • 개인 SDK를 사용하거나 사용하지 않을 수 있습니다

나는 "애플이 그것을 허용하지 않는다"고 말하면서 대답을 원하지 않는다. 나는 그것을 수행하는 공식적인 방법이없고, 앱 스토어 애플리케이션을위한 것이 아니라는 것을 알고 있으며, 자체 서버를 통해 나가는 통화를하는 콜 레코딩 앱이 있다는 것을 알고 있습니다.

도움이 되었습니까?

해결책

당신은 간다. 완전한 작업 예제. 조정을로드해야합니다 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);
}

무슨 일이 일어나고 있는지에 대한 몇 마디. AudioUnitProcess 기능은 일부 효과, 믹스, 변환 등을 적용하기 위해 오디오 스트림 처리에 사용됩니다. AudioUnitProcess 전화 통화의 오디오 스트림에 액세스하기 위해 전화 통화가 활성화되어있는 동안 이러한 스트림은 다양한 방식으로 처리되고 있습니다.

전화 통화 상태 변경을 얻기 위해 Coretelephony 알림을 듣고 있습니다. 오디오 샘플을 받으면 마이크 또는 스피커에서 오는 위치를 결정해야합니다. 이것은 사용합니다 componentSubType 필드에 AudioComponentDescription 구조. 이제, 당신은 생각할 것입니다. 왜 우리가 저장하지 않습니까? AudioUnit 확인할 필요가없는 개체 componentSubType 매번. 나는 그렇게했지만 iPhone 5에서 스피커를 켜거나 끄면 모든 것을 깨뜨릴 것입니다. AudioUnit 물체가 바뀌고 재현됩니다. 이제 우리는 오디오 파일 (마이크 용 및 스피커 용)을 열고 간단하게 샘플을 작성합니다. 전화가 종료되면 적절한 Coretelephony 알림을 받고 파일을 닫습니다. 우리는 마이크와 스피커의 오디오가있는 두 개의 별도 파일이있어 병합해야합니다. 이것이 무엇입니다 void Convert() 입니다. API를 알고 있다면 매우 간단합니다. 나는 그것을 설명 할 필요가 없다고 생각한다.

자물쇠에 대해. 많은 스레드가 있습니다 mediaserverd. 오디오 처리 및 Coretelephony 알림은 다른 스레드에 있으므로 일종의 동기화가 필요합니다. 스핀 잠금 장치는 빠르고 우리의 경우 자물쇠 경합 가능성이 작기 때문에 선택했습니다. iPhone 4S 및 심지어 iPhone 5에서 모든 작업 AudioUnitProcess 그렇지 않으면 가능한 한 빨리 수행해야합니다. 그렇지 않으면 장치 스피커의 딸꾹질을들을 수 있습니다.

다른 팁

예. 오디오 녹음기 Limneos라는 개발자는 그렇게합니다. Cydia에서 찾을 수 있습니다. 서버 등을 사용하지 않고 iPhone 5 이상에서 모든 유형의 통화를 기록 할 수 있습니다. 호출은 오디오 파일의 장치에 배치됩니다. 또한 iPhone 4를 지원하지만 스피커만을 지원합니다.

이 조정은 제 3 자 세버, VoIP 또는 이와 유사한 것을 사용하지 않고 두 오디오 스트림을 녹음 할 수있는 최초의 조정으로 알려져 있습니다.

개발자는 전화의 반대편에 경고음을두고 녹음중인 사람에게 경고하지만 그물을 가로 질러 해커들에 의해서도 제거되었습니다. 귀하의 질문에 답하기 위해, 예, 이론적으로뿐만 아니라 가능합니다.

enter image description here

추가 독서

내가 생각할 수있는 유일한 해결책은 핵심 전화 프레임 워크,보다 구체적으로 칼리 벤틀 러 러 부동산, 전화가 올 때 가로 채고 다음을 사용합니다. avaudiorecorder 전화로 사람의 목소리를 녹음하기 위해 (아마도 다른 줄의 목소리에있는 사람이있을 수도 있습니다). 이것은 분명히 완벽하지 않으며, 신청이 통화 당시 전경에있는 경우에만 작동하지만 최선을 다할 수 있습니다. 들어오는 전화가 있는지 확인하는 것에 대해 자세히 알아보십시오. iPhone에 들어오는 전화가있을 때 이벤트를 해고 할 수 있습니까?.

편집하다:

.시간:

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

이러한 기능 중 많은 기능을 사용하는 것은 이번이 처음이므로 이것이 옳은지 확실하지 않지만 아이디어를 얻는 것 같습니다. 현재 올바른 도구에 액세스 할 수 없으므로 테스트되지 않았습니다. 이러한 소스를 사용하여 편집 :

Apple은 그것을 허용하지 않으며 API를 제공하지 않습니다.

그러나 탈옥 장치에서는 가능할 것이라고 확신합니다. 사실, 나는 이미 끝났다고 생각합니다. 내 전화가 탈옥 된 앱을 본 것을 기억합니다. 불행히도 나는 그 이름을 기억하지 못한다 ...

나는 일부 하드웨어가 이것을 해결할 수 있다고 생각합니다. 미니 잭 포트에 연결; 작은 레코더를 통과하는 이어 버드와 마이크가 있습니다. 이 레코더는 매우 간단 할 수 있습니다. 대화 중에는 레코더가 데이터/레코딩 (Jack-Cable)으로 전화를 공급할 수 있습니다. 간단한 시작 버튼 (이어 버드의 볼륨 컨트롤과 마찬가지로)을 사용하면 녹음시기에 충분할 수 있습니다.

일부 설정

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top