iOS之視頻語音通話(二)編碼H264+MP3

界面

- (void)openVideoCapture {
    self.session = [[AVCaptureSession alloc] init];
    [self.session setSessionPreset:AVCaptureSessionPreset640x480];
    AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
    UIView *cameraView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    previewLayer.frame = cameraView.bounds;
    [cameraView.layer addSublayer:previewLayer];
    [self.view addSubview:cameraView];
    // 攝像頭以及視頻輸入
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    videoDevice = [self cameraWithPosition:AVCaptureDevicePositionFront];
    NSError *error = nil;
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    if ([self.session canAddInput:videoInput]) {
        [self.session addInput:videoInput];
    }
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    videoOutput.videoSettings = [NSDictionary dictionaryWithObject:
                                 [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]forKey:(NSString *)kCVPixelBufferPixelFormatTypeKey];
    [videoOutput setAlwaysDiscardsLateVideoFrames:YES];
    if ([_session canAddOutput:videoOutput] == NO)
    {
        YGNLog(@"Couldn't add video output");
        return ;
    }
    [_session addOutput:videoOutput];
    _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];// 設(shè)置采集圖像的方向,如果不設(shè)置,采集回來的圖形會是旋轉(zhuǎn)90度的
    _videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    
    dispatch_queue_t queue = dispatch_queue_create("VideoCaptureQueue", DISPATCH_QUEUE_SERIAL);// 攝像頭采集queue
    [videoOutput setSampleBufferDelegate:self queue:queue];
    _h264File = fopen([[NSString stringWithFormat:@"%@/vt_encode.h264", self.documentDictionary] UTF8String], "wb");
    //麥克風以及音頻輸入
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
    if ([self.session canAddInput:audioInput]) {
        [self.session addInput:audioInput];
    }
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    
    dispatch_queue_t queue1 = dispatch_queue_create("AudioCaptureQueue", DISPATCH_QUEUE_SERIAL);// 攝像頭采集queue
    [audioOutput setSampleBufferDelegate:self queue:queue1];
    if ([_session canAddOutput:audioOutput] == NO)
    {
        YGNLog(@"Couldn't add audio output");
        return ;
    }
    _audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio];
    [_session addOutput:audioOutput];
    // 文件保存在document文件夾下,可以直接通過iTunes將文件導(dǎo)出到電腦,在plist文件中添加Application supports iTunes file sharing = YES
   [self.session commitConfiguration];
    return ;
}

代理方法

#pragma mark - AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    
    if (_mediuChatType == YGMessageChatCallType_AudioReceive || _mediuChatType == YGMessageChatCallType_AudioCall) {
//        NSData *PCMData = [self convertAudioSmapleBufferToPcmData:sampleBuffer];
        if (ipTestString != nil && [YGUDPNatManager sharedManager].isUdpNatTranting == YES) {
//            YGNLog(@"PCMData.length======%lu",(unsigned long)PCMData.length);
//            [[YGUDPNatManager sharedManager] sendTestData:PCMData withIP:ipTestString withPort:1234];
        }
    } else {
        //視頻采集
        if(connection == _videoConnection){
            [self encodeFrame:sampleBuffer];
            
        }else{
            NSData* data =  [self convertAudioSmapleBufferToPcmData:sampleBuffer];
            if(data) {
                [_audioData appendData:data];
            }
           
        }
    }
}

音頻MP3處理

//提取pcm
-(NSData *) convertAudioSmapleBufferToPcmData:(CMSampleBufferRef) audioSample{
    //獲取pcm數(shù)據(jù)大小
    NSInteger audioDataSize = CMSampleBufferGetTotalSampleSize(audioSample);
    
    //分配空間
    int8_t *audio_data = malloc((int32_t)audioDataSize);
    
    //獲取CMBlockBufferRef
    //這個結(jié)構(gòu)里面就保存了 PCM數(shù)據(jù)
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(audioSample);
    //直接將數(shù)據(jù)copy至我們自己分配的內(nèi)存中
    CMBlockBufferCopyDataBytes(dataBuffer, 0, audioDataSize, audio_data);
    NSData *pcmData = [NSData dataWithBytesNoCopy:audio_data length:audioDataSize];
    //返回數(shù)據(jù)
    return pcmData;
}

#pragma mark ---------pcm 轉(zhuǎn)MP3

- (void)conventToMp3 {
    NSLog(@"convert begin!!");
    NSString *cafFilePath = [[YGMessageMediaUtily sharedMediaUtily] stringPathWithType:YGMessageMediaType_ImFile sessionID:0 mType:0 fileName:@"vt_encode.pcm"];
    NSString *mp3FilePath = [[NSHomeDirectory() stringByAppendingFormat:@"/Documents/"] stringByAppendingPathComponent:@"vy_encode.mp3"];
    @try {
        int read, write;
        FILE *pcm = fopen([cafFilePath cStringUsingEncoding:NSASCIIStringEncoding], "rb");
        fseek(pcm, 4*1024, SEEK_CUR);
        FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:NSASCIIStringEncoding], "wb");
        const int PCM_SIZE = 8192;
        const int MP3_SIZE = 8192;
        short int pcm_buffer[PCM_SIZE * 2];
        unsigned char mp3_buffer[MP3_SIZE];
        lame_t lame = lame_init();
        lame_set_in_samplerate(lame,22000);//采樣播音速度,值越大播報速度越快,反之。
        lame_set_brate(lame,128);
        lame_set_num_channels(lame, 2);
        lame_set_mode(lame,MONO);
        lame_set_quality (lame, 2); /* 2=high 5 = medium 7=low 音 質(zhì) */
        lame_set_VBR(lame, vbr_default);
        lame_init_params(lame);
        do {
            read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm);
            if (read == 0)
                write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
            else
                write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
            
            fwrite(mp3_buffer, write, 1, mp3);
            
        } while (read != 0);

        lame_close(lame);
        fclose(mp3);
        fclose(pcm);
    }
    @catch (NSException *exception) {
        NSLog(@"%@", [exception description]);
    }
    @finally {
        NSLog(@"convert mp3 finish!!!");
    }
}
===========掛斷的時候調(diào)用==============
 [YGUDPNatManager sharedManager].isUdpNatTranting = YES;
    [[YGMessageMediaUtily sharedMediaUtily] storeData:_audioData forFileName:@"vt_encode.pcm" sessionID:0 type:YGMessageMediaType_ImFile mType:0];//存儲數(shù)據(jù)到本地
    [self conventToMp3];

視頻h264處理

- (int)startEncodeSession:(int)width height:(int)height framerate:(int)fps bitrate:(int)bt
{
    OSStatus status;
    _frameCount = 0;
    
    VTCompressionOutputCallback cb = encodeOutputCallback;
    status = VTCompressionSessionCreate(kCFAllocatorDefault, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, cb, (__bridge void *)(self), &_encodeSesion);
    
    if (status != noErr) {
        NSLog(@"VTCompressionSessionCreate failed. ret=%d", (int)status);
        return -1;
    }
    
    // 設(shè)置實時編碼輸出,降低編碼延遲
    status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    NSLog(@"set realtime  return: %d", (int)status);
    
    // h264 profile, 直播一般使用baseline,可減少由于b幀帶來的延時
    status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    NSLog(@"set profile   return: %d", (int)status);
    
    // 設(shè)置編碼碼率(比特率),如果不設(shè)置,默認將會以很低的碼率編碼,導(dǎo)致編碼出來的視頻很模糊
    status  = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bt)); // bps
    status += VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(bt*2/8), @1]); // Bps
    NSLog(@"set bitrate   return: %d", (int)status);
    
    // 設(shè)置關(guān)鍵幀間隔,即gop size
    status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(fps*2));
    
    // 設(shè)置幀率,只用于初始化session,不是實際FPS
    status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(fps));
    NSLog(@"set framerate return: %d", (int)status);
    
    // 開始編碼
    status = VTCompressionSessionPrepareToEncodeFrames(_encodeSesion);
    NSLog(@"start encode  return: %d", (int)status);
    
    return 0;
}
// 編碼一幀圖像,使用queue,防止阻塞系統(tǒng)攝像頭采集線程
- (void) encodeFrame:(CMSampleBufferRef )sampleBuffer
{
    dispatch_sync(_encodeQueue, ^{
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        
        // pts,必須設(shè)置,否則會導(dǎo)致編碼出來的數(shù)據(jù)非常大,原因未知
        CMTime pts = CMTimeMake(_frameCount, 1000);
        CMTime duration = kCMTimeInvalid;
        
        VTEncodeInfoFlags flags;
        
        // 送入編碼器編碼
        OSStatus statusCode = VTCompressionSessionEncodeFrame(_encodeSesion,
                                                              imageBuffer,
                                                              pts, duration,
                                                              NULL, NULL, &flags);
        
        if (statusCode != noErr) {
            NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
            
            [self stopEncodeSession];
            return;
        }
    });
}
- (void) stopEncodeSession
{
    VTCompressionSessionCompleteFrames(_encodeSesion, kCMTimeInvalid);
    
    VTCompressionSessionInvalidate(_encodeSesion);
    
    CFRelease(_encodeSesion);
    _encodeSesion = NULL;
}
// 編碼回調(diào),每當系統(tǒng)編碼完一幀之后,會異步掉用該方法,此為c語言方法
void encodeOutputCallback(void *userData, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags,
                          CMSampleBufferRef sampleBuffer )
{
    if (status != noErr) {
        NSLog(@"didCompressH264 error: with status %d, infoFlags %d", (int)status, (int)infoFlags);
        return;
    }
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
        NSLog(@"didCompressH264 data is not ready ");
        return;
    }
    YGVideoChatViewController* vc = (__bridge YGVideoChatViewController*)userData;
    
    // 判斷當前幀是否為關(guān)鍵幀
    bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    // 獲取sps & pps數(shù)據(jù). sps pps只需獲取一次,保存在h264文件開頭即可
    if (keyframe && !vc->_spsppsFound)
    {
        size_t spsSize, spsCount;
        size_t ppsSize, ppsCount;
        
        const uint8_t *spsData, *ppsData;
        
        CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
        OSStatus err0 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0 );
        OSStatus err1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0 );
        
        if (err0==noErr && err1==noErr)
        {
            vc->_spsppsFound = 1;
            [vc writeH264Data:(void *)spsData length:spsSize addStartCode:YES];
            [vc writeH264Data:(void *)ppsData length:ppsSize addStartCode:YES];
            NSLog(@"got sps/pps data. Length: sps=%zu, pps=%zu", spsSize, ppsSize);
        }
    }
    
    size_t lengthAtOffset, totalLength;
    char *data;
    
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    OSStatus error = CMBlockBufferGetDataPointer(dataBuffer, 0, &lengthAtOffset, &totalLength, &data);
    
    if (error == noErr) {
        size_t offset = 0;
        const int lengthInfoSize = 4; // 返回的nalu數(shù)據(jù)前四個字節(jié)不是0001的startcode,而是大端模式的幀長度length
        
        // 循環(huán)獲取nalu數(shù)據(jù)
        while (offset < totalLength - lengthInfoSize) {
            uint32_t naluLength = 0;
            memcpy(&naluLength, data + offset, lengthInfoSize); // 獲取nalu的長度,
            // 大端模式轉(zhuǎn)化為系統(tǒng)端模式
            naluLength = CFSwapInt32BigToHost(naluLength);
            
            NSLog(@"got nalu data, length=%d, totalLength=%zu", naluLength, totalLength);
            // 保存nalu數(shù)據(jù)到文件
            [vc writeH264Data:data+offset+lengthInfoSize length:naluLength addStartCode:YES];
            // 讀取下一個nalu,一次回調(diào)可能包含多個nal H264: VTCompressionSessionEncodeFrame failed with -12902u
            offset += lengthInfoSize + naluLength;
        }
    }
}

// 保存h264數(shù)據(jù)到文件
- (void) writeH264Data:(void*)data length:(size_t)length addStartCode:(BOOL)b
{
    // 添加4字節(jié)的 h264 協(xié)議 start code
    const Byte bytes[] = "\x00\x00\x00\x01";
    
    if (_h264File) {
        if(b)
            fwrite(bytes, 1, 4, _h264File);
        
        fwrite(data, 1, length, _h264File);
        NSLog(@"save success");
    } else {
        NSLog(@"_h264File null error, check if it open successed");
    }
}


-  (void)viewDidAppear:(BOOL)animated {
    [self startEncodeSession:480 height:640 framerate:25 bitrate:640*1000];
    [_session startRunning];
}

總結(jié)

此處視頻是實時轉(zhuǎn),視頻中的音頻是先存為pcm文件。然后掛斷的時候?qū)?pcm 轉(zhuǎn)為MP3。另外,在轉(zhuǎn)MP3
的時候 采樣率以及相關(guān)參數(shù)一定要設(shè)置對。如果有變音就是采樣率的問題。有問題或有補充的盆友歡迎底下留言。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容