كيف يمكنني تسجيل محادثة / مكالمة هاتفية على نظام iOS؟

StackOverflow https://stackoverflow.com/questions/1809347

سؤال

هل من الممكن نظريًا تسجيل مكالمة هاتفية على iPhone؟

أنا أقبل الإجابات التي:

  • قد يتطلب أو لا يتطلب كسر حماية الهاتف
  • قد يتم أو لا يتم تمرير إرشادات Apple بسبب استخدام واجهات برمجة التطبيقات الخاصة (لا أهتم؛إنه ليس لمتجر التطبيقات)
  • قد يستخدم أو لا يستخدم أدوات تطوير البرامج (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 4S ولكن للسماعات فقط.

من المعروف أن هذا التعديل هو أول قرص على الإطلاق تمكن من تسجيل كلا التدفقين الصوتيين دون استخدام أي خوادم تابعة لجهات خارجية أو VOIP أو شيء مشابه.

قام المطور بوضع أصوات تنبيه على الجانب الآخر من المكالمة لتنبيه الشخص الذي تقوم بتسجيله، ولكن تمت إزالتها أيضًا بواسطة المتسللين عبر الشبكة.للإجابة على سؤالك، نعم، هذا ممكن جدًا، وليس فقط من الناحية النظرية.

enter image description here

قراءة متعمقة

الحل الوحيد الذي يمكنني التفكير فيه هو استخدام الهاتفية الأساسية الإطار، وبشكل أكثر تحديدا callEventHandler الخاصية، لاعتراض مكالمة واردة، ومن ثم استخدام AVaudioRecorder لتسجيل صوت الشخص بالهاتف (وربما القليل من صوت الشخص على الخط الآخر).من الواضح أن هذا ليس مثاليًا، ولن ينجح إلا إذا كان طلبك في المقدمة وقت المكالمة، ولكنه قد يكون أفضل ما يمكنك الحصول عليه.تعرف على المزيد حول معرفة ما إذا كانت هناك مكالمة هاتفية واردة هنا: هل يمكننا إطلاق حدث عندما تكون هناك مكالمة واردة وصادرة في iPhone؟.

يحرر:

.ح:

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

عرضDidLoad:

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) له.

ومع ذلك، على جهاز مكسور الحماية، أنا متأكد من أن ذلك ممكن.في واقع الأمر، أعتقد أن الأمر قد تم بالفعل.أتذكر رؤية أحد التطبيقات عندما تم كسر حماية هاتفي لتغيير صوتك وتسجيل المكالمة - أتذكر أنها كانت شركة أمريكية تقدم هذا التطبيق في الولايات المتحدة فقط.للأسف لا أذكر الاسم..

أعتقد أن بعض الأجهزة يمكن أن تحل هذه المشكلة.متصل بمنفذ minijack؛وجود سماعات أذن وميكروفون يمر عبر مسجل صغير.يمكن أن يكون هذا المسجل بسيطًا جدًا.أثناء عدم وجود محادثة، يمكن للمسجل تغذية الهاتف بالبيانات/التسجيل (من خلال كابل المقبس).وباستخدام زر بدء بسيط (تمامًا مثل عناصر التحكم في مستوى الصوت في سماعات الأذن) يمكن أن يكون كافيًا لتوقيت التسجيل.

بعض الاعداد

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top