iOS 音视频采集及rtmp推流

项目暂时告一段落,也是一知半解,不过我的分享可以帮助我这样菜鸟了。
先来下知识结构:

1、h264

视频编码处理的最后一步就是熵编码,在H.264中采用了两种不同的熵编码方法:通用可变长编码(UVLC)和基于文本的自适应二进制算术编码(CABAC)。

2、aac

Advanced Audio Coding。一种专为声音数据设计的文件压缩格式,与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低

3、pcm

音频采集的原始数据,硬编码数据

4、yuv

视频采集的原始数据,硬编码数据

5、时间戳

直播音视频同步的关键参数

6、rtmp推流

直播的推流手段

一、我们首先要做的就是采集,我们需要采集硬编码数据yuv将其转化成h264,然后采集pcm数据,并将其转化成aac数据,并发送

1、视频采集,我们要采集最后要转化为h264编码的格式,需要用到VideoToolbox.framework及AVFoundation.framework
VideoToolbox.framework 的主要工作是编码,将yuv数据编码为h264。AVFoundation.framework的任务是采集yuv原始数据。

// 获取硬编码数据函数,一些初始化工作就不在这里熬述了,网上有很多
-(void) captureOutput:(AVCaptureOutput*)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection
{
}

(1)初始化VTCompressionSession。
VTCompressionSession初始化的时候需要给出width宽,height长,编码器类型kCMVideoCodecType_H264等。然后通过调用VTSessionSetProperty接口设置帧率等属性,最后需要设定一个回调函数,这个回调是视频图像编码成功后调用。全部准备好后,使用VTCompressionSessionCreate创建session。

// 这个函数是初始化
- (void) initEncode:(int)width  height:(int)height bite:(int)iBite
{
    dispatch_sync(aQueue, ^{
        
        // For testing out the logic, lets read from a file and then send it to encoder to create h264 stream
        
        // Create the compression session   注意h264函数
        OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &EncodingSession);
        NSLog(@"H264: VTCompressionSessionCreate %d", (int)status);
        
        if (status != 0)
        {
            NSLog(@"H264: Unable to create a H264 session");
            error = @"H264: Unable to create a H264 session";
            
            return ;
            
        }
        
        // 码率是清晰度
        // Set the properties
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nonnull)(@(GOP_SIZE)));
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nonnull)@(iBite));
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nonnull)@(FRAME_RATE));
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFTypeRef _Nonnull)@[@(iBite/8),@(1)]);
    
        // Tell the encoder to start encoding
        VTCompressionSessionPrepareToEncodeFrames(EncodingSession);
    });
}

(2)提取摄像头采集的原始图像数据给VTCompressionSession来硬编码。

摄像头采集后的图像是未编码的CMSampleBuffer形式,利用给定的接口函数CMSampleBufferGetImageBuffer从中提取出CVPixelBufferRef,使用硬编码接口VTCompressionSessionEncodeFrame来对该帧进行硬编码,编码成功后,会自动调用session初始化时设置的回调函数。

    dispatch_sync(aQueue, ^{
        
        frameCount++;
        // Get the CV Image buffer  提取摄像头采集的原始图像数据给VTCompressionSession来硬编码 也就是给VTCompressionSessionCreate来编码
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        
        // Create properties
        CMTime presentationTimeStamp = CMTimeMake(frameCount, 1000);
        //CMTime duration = CMTimeMake(1, DURATION);
        VTEncodeInfoFlags flags;
        
        // Pass it to the encoder
        OSStatus statusCode = VTCompressionSessionEncodeFrame(EncodingSession,
                                                              imageBuffer,
                                                              presentationTimeStamp,
                                                              kCMTimeInvalid,
                                                              NULL, NULL, &flags);
        // Check for error
        if (statusCode != noErr) {
            NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
            error = @"H264: VTCompressionSessionEncodeFrame failed ";
            
            // End the session
            VTCompressionSessionInvalidate(EncodingSession);
            CFRelease(EncodingSession);
            EncodingSession = NULL;
            error = NULL;
            return;
        }
        //            NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    });

(3)利用回调函数,将因编码成功的CMSampleBuffer转换成H264码流,通过网络传播。
基本上是硬解码的一个逆过程。

void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags,
                     CMSampleBufferRef sampleBuffer )
{
//        NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
    NSLog(@"H264");
    if (status != 0) return;
    
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
        NSLog(@"didCompressH264 data is not ready ");
        return;
    }
    H264Encoder* encoder = (__bridge H264Encoder*)outputCallbackRefCon;
    
    // Check if we have got a key frame first
    bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    encoder->countFrame=encoder->countFrame+1;
    
//    NSLog(@"dzf  frameCount%d",encoder->countFrame);
    if (keyframe)
    {
//        NSLog(@"dzf  keyframe is true ");
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        // CFDictionaryRef extensionDict = CMFormatDescriptionGetExtensions(format);
        // Get the extensions
        // From the extensions get the dictionary with key "SampleDescriptionExtensionAtoms"
        // From the dict, get the value for the key "avcC"
        
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        if (statusCode == noErr)
        {
            // Found sps and now check for pps
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
            if (statusCode == noErr)
            {
                // Found pps
                encoder->sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                encoder->pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                if (encoder->_delegate)
                {
                    [encoder->_delegate gotSpsPps:encoder->sps pps:encoder->pps];
                }
            }
        }
    }
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {

        // 发送数据
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4;
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            
            // Read the NAL unit length
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // Convert the length value from Big-endian to Little-endian
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder->_delegate gotEncodedData:data isKeyFrame:keyframe];
            
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
        // 你小子存的数据
        [encoder->_delegate oneFrameEncodeEnd:keyframe];
    }
}

值得注意的是一段视频的头部是sps pps 组成的,我们在这个函数中要检查头部信息,筛选普通信息进行封装发送推流。先发送头部数据再发送普通视频数据。
解析出参数集SPS和PPS,加上开始码后组装成NALU。提取出视频数据,将长度码转换成开始码,组长成NALU。将NALU发送出去。

发送视频头部信息代码

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
//    NSLog(@"gotSpsPps");
    frameCount2 = [_h264Encoder getFreameCound];
    
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    mysps = sps;
    mypps = pps;
    [mutableData appendData:ByteHeader];
    [mutableData appendData:mysps];
    [mutableData appendData:ByteHeader];
    [mutableData appendData:mypps];
    pos = pos + sps.length + pps.length + ByteHeader.length*2;
    
    NSMutableData *mutableDataTem1 = [[NSMutableData alloc] init];;
    [mutableDataTem1 appendData:ByteHeader];
    [mutableDataTem1 appendData:mysps];
    long tem1 = sps.length + ByteHeader.length;
    [self sendData:sizeof(Byte)*tem1 data:(char*)[mutableDataTem1 bytes]];
    
    NSMutableData *mutableDataTem = [[NSMutableData alloc] init];;
    [mutableDataTem appendData:ByteHeader];
    [mutableDataTem appendData:mypps];
    long tem = pps.length + ByteHeader.length;
    [self sendData:sizeof(Byte)*tem data:(char*)[mutableDataTem bytes]];
}

发送实体部分代码

- (void)oneFrameEncodeEnd:(BOOL)isKeyFrame
{
    FrameData *frameData = [[FrameData alloc] init];
    
    frameData.Iframe = isKeyFrame;
    frameData.frame_len = (int) pos;
    frameData.frame_seq = total_vseq;
    frameData.stream_index = 0;
    
    
    frameData.frame_data = (Byte *)malloc(sizeof(Byte)*pos);//new Byte[pos];
    memcpy(frameData.frame_data,[mutableData bytes], pos*sizeof(Byte));
    
    [_videoArray addObject:frameData];
    total_vseq++;
    
    //if(isKeyFrame)
    //NSLog(@"add one h264 h264  h264  frame to videoArray---seq:%ld",total_vseq);
    
    mysps = nil;
    mypps = nil;
    
    [mutableData resetBytesInRange:NSMakeRange(0, [mutableData length])];
    [mutableData setLength:0];
    
    pos = 0;
}

音频的采集发送

将采集pcm数据进行aac编码,网上应该有相关的代码可以学习

-(void) captureOutput:(AVCaptureOutput*)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection
{
    static BOOL             firstStartTimer = false;
    static long             num = 0;

    if (connection == _audioConnection) {
        NSLog(@"captureOutput audio");
        
        char szBuf[4096];
        memset(szBuf, 0, sizeof(szBuf));
        uint32_t  nSize = sizeof(szBuf);
//        AudioStreamBasicDescription inputFormat = *(CMAudioFormatDescriptionGetStreamBasicDescription(CMSampleBufferGetFormatDescription(sampleBuffer))); // 输入音频格式
        
        AudioStreamBasicDescription outputFormat = *(CMAudioFormatDescriptionGetStreamBasicDescription(CMSampleBufferGetFormatDescription(sampleBuffer)));
        nSize = CMSampleBufferGetTotalSampleSize(sampleBuffer);
        CMBlockBufferRef databuf = CMSampleBufferGetDataBuffer(sampleBuffer);
        if (CMBlockBufferCopyDataBytes(databuf, 0, nSize, szBuf) == kCMBlockBufferNoErr)
        {
            int32_t nOffSet = 0;
            while (nOffSet < nSize)
            {
                int outsize = 0;
                char szOutBuf[4096] = {0};
                
                int nInSize = 0;
                if (nSize - nOffSet >= 640) {
                    nInSize = 640;
                } else {
                    nInSize = nSize - nOffSet;
                }
                
                outsize = [ecdoer AACEncoderEncode:lHand inData:szBuf + nOffSet inSize:nInSize outData:szOutBuf maxOutSize:4096];
                //            [ecdoer AACEncoderClose:outsize];
                if (outsize > 0)
                {
                    [self sendAacDataLen:outsize data:szOutBuf ptsTime:0];
                }

                nOffSet += 640;
            }

        }

    }
}

音频数据的发送

- (void)sendAacDataLen:(int) totalLength data: (char*) dataPointer ptsTime:(int64_t)pts{

    int ret = WM_RTMPLIVESDK_InputData(WMRtmpLiveDataType_AAC, (const char* )dataPointer, totalLength, [self getNowTime]);
    NSLog(@"~~~~~~~~~iAAc[%lld]",[self getNowTime]);
    if (ret == 1) {
        NSLog(@"~~~~~aac~~~~~sendData ret[%d] totalLength[%d]",ret,(int)totalLength);
    }
    // fail 1 success 0
}

rtmp推流网上也有很多代码,调用rtmplib 可以自己用c++封装一个库用来调用。

二、最后的陈述

这里就先不解释了大体采集发送的过程就是这样,还有一点视频采集发送音频采集发送的时间获取的是当前时间,测试的时候也可以写间隔20ms来测试延迟的问题,它的逻辑是发送一堆音频数据再发送一个视频数据,因为音频数据比较多,音频数据如果丢帧会感觉出来明显的卡顿,视频则不是,视频丢一帧人眼是很难发现的
有些详细的理论推荐大家看这篇博客。
//www.greatytc.com/p/a6530fa46a88

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容