美文网首页即时通迅和直播iOS 直播视频ios视频直播
1小时学会:最简单的iOS直播推流(七)h264/aac 硬编码

1小时学会:最简单的iOS直播推流(七)h264/aac 硬编码

作者: hard_man | 来源:发表于2016-12-07 21:48 被阅读3202次

    最简单的iOS 推流代码,视频捕获,软编码(faac,x264),硬编码(aac,h264),美颜,flv编码,rtmp协议,陆续更新代码解析,你想学的知识这里都有,愿意懂直播技术的同学快来看!!

    源代码:https://github.com/hardman/AWLive

    前面已经介绍了如何从硬件设备获取到音视频数据(pcm,NV12)。

    但是我们需要的视频格式是 aac和 h264。

    现在就介绍一下如何将pcm编码aac,将NV12数据编码为h264。

    编码分为软编码和硬编码。

    硬编码是系统提供的,由系统专门嵌入的硬件设备处理音视频编码,主要计算操作在对应的硬件中。硬编码的特点是,速度快,cpu占用少,但是不够灵活,只能使用一些特定的功能。

    软编码是指,通过代码计算进行数据编码,主要计算操作在cpu中。软编码的特点是,灵活,多样,功能丰富可扩展,但是cpu占用较多。

    在代码中,编码器是通过AWEncoderManager获取的。

    AWENcoderManager是一个工厂,通过audioEncoderType和videoEncoderType指定编码器类型。

    编码器分为两类,音频编码器(AWAudioEncoder),视频编码器(AWVideoEncoder)。

    音视频编码器又分别分为硬编码(在HW目录中)和软编码(在SW目录中)。

    所以编码部分主要有4个文件:硬编码H264(AWHWH264Encoder),硬编码AAC(AWHWAACEncoder),软编码AAC(AWSWFaacEncoder),软编码H264(AWSWX264Encoder)

    硬编码H264

    第一步,开启硬编码器

    -(void)open{
        //创建 video encode session
        // 创建 video encode session
        // 传入视频宽高,编码类型:kCMVideoCodecType_H264
        // 编码回调:vtCompressionSessionCallback,这个回调函数为编码结果回调,编码成功后,会将数据传入此回调中。
        // (__bridge void * _Nullable)(self):这个参数会被原封不动地传入vtCompressionSessionCallback中,此参数为编码回调同外界通信的唯一参数。
        // &_vEnSession,c语言可以给传入参数赋值。在函数内部会分配内存并初始化_vEnSession。
        OSStatus status = VTCompressionSessionCreate(NULL, (int32_t)(self.videoConfig.pushStreamWidth), (int32_t)self.videoConfig.pushStreamHeight, kCMVideoCodecType_H264, NULL, NULL, NULL, vtCompressionSessionCallback, (__bridge void * _Nullable)(self), &_vEnSession);
        if (status == noErr) {
            // 设置参数
            // ProfileLevel,h264的协议等级,不同的清晰度使用不同的ProfileLevel。
            VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
            // 设置码率
            VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(self.videoConfig.bitrate));
            // 设置实时编码
            VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
            // 关闭重排Frame,因为有了B帧(双向预测帧,根据前后的图像计算出本帧)后,编码顺序可能跟显示顺序不同。此参数可以关闭B帧。
            VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
            // 关键帧最大间隔,关键帧也就是I帧。此处表示关键帧最大间隔为2s。
            VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(self.videoConfig.fps * 2));
            // 关于B帧 P帧 和I帧,请参考:http://blog.csdn.net/abcjennifer/article/details/6577934
            
            //参数设置完毕,准备开始,至此初始化完成,随时来数据,随时编码
            status = VTCompressionSessionPrepareToEncodeFrames(_vEnSession);
            if (status != noErr) {
                [self onErrorWithCode:AWEncoderErrorCodeVTSessionPrepareFailed des:@"硬编码vtsession prepare失败"];
            }
        }else{
            [self onErrorWithCode:AWEncoderErrorCodeVTSessionCreateFailed des:@"硬编码vtsession创建失败"];
        }
    }
    

    第二步,向编码器丢数据:

    //这里的参数yuvData就是从相机获取的NV12数据。
    -(aw_flv_video_tag *)encodeYUVDataToFlvTag:(NSData *)yuvData{
        if (!_vEnSession) {
            return NULL;
        }
        //yuv 变成 转CVPixelBufferRef
        OSStatus status = noErr;
        
        //视频宽度
        size_t pixelWidth = self.videoConfig.pushStreamWidth;
        //视频高度
        size_t pixelHeight = self.videoConfig.pushStreamHeight;
    
        //现在要把NV12数据放入 CVPixelBufferRef中,因为 硬编码主要调用VTCompressionSessionEncodeFrame函数,此函数不接受yuv数据,但是接受CVPixelBufferRef类型。
        CVPixelBufferRef pixelBuf = NULL;
        //初始化pixelBuf,数据类型是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,此类型数据格式同NV12格式相同。
        CVPixelBufferCreate(NULL, pixelWidth, pixelHeight, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, NULL, &pixelBuf);
        
        // Lock address,锁定数据,应该是多线程防止重入操作。
        if(CVPixelBufferLockBaseAddress(pixelBuf, 0) != kCVReturnSuccess){
            [self onErrorWithCode:AWEncoderErrorCodeLockSampleBaseAddressFailed des:@"encode video lock base address failed"];
            return NULL;
        }
        
        //将yuv数据填充到CVPixelBufferRef中
        size_t y_size = pixelWidth * pixelHeight;
        size_t uv_size = y_size / 4;
        uint8_t *yuv_frame = (uint8_t *)yuvData.bytes;
        
        //处理y frame
        uint8_t *y_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuf, 0);
        memcpy(y_frame, yuv_frame, y_size);
        
        uint8_t *uv_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuf, 1);
        memcpy(uv_frame, yuv_frame + y_size, uv_size * 2);
        
        //硬编码 CmSampleBufRef
        
        //时间戳
        uint32_t ptsMs = self.manager.timestamp + 1; //self.vFrameCount++ * 1000.f / self.videoConfig.fps;
        
        CMTime pts = CMTimeMake(ptsMs, 1000);
        
        //硬编码主要其实就这一句。将携带NV12数据的PixelBuf送到硬编码器中,进行编码。
        status = VTCompressionSessionEncodeFrame(_vEnSession, pixelBuf, pts, kCMTimeInvalid, NULL, pixelBuf, NULL);
    
        ... ...
    }
    

    第三步,通过硬编码回调获取h264数据

    static void vtCompressionSessionCallback (void * CM_NULLABLE outputCallbackRefCon,
                                              void * CM_NULLABLE sourceFrameRefCon,
                                              OSStatus status,
                                              VTEncodeInfoFlags infoFlags,
                                              CM_NULLABLE CMSampleBufferRef sampleBuffer ){
        //通过outputCallbackRefCon获取AWHWH264Encoder的对象指针,将编码好的h264数据传出去。
        AWHWH264Encoder *encoder = (__bridge AWHWH264Encoder *)(outputCallbackRefCon);
    
        //判断是否编码成功
        if (status != noErr) {
            dispatch_semaphore_signal(encoder.vSemaphore);
            [encoder onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error 1"];
            return;
        }
        
        //是否数据是完整的
        if (!CMSampleBufferDataIsReady(sampleBuffer)) {
            dispatch_semaphore_signal(encoder.vSemaphore);
            [encoder onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error 2"];
            return;
        }
        
        //是否是关键帧,关键帧和非关键帧要区分清楚。推流时也要注明。 
        BOOL isKeyFrame = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
        
        //首先获取sps 和pps
        //sps pss 也是h264的一部分,可以认为它们是特别的h264视频帧,保存了h264视频的一些必要信息。
        //没有这部分数据h264视频很难解析出来。
        //数据处理时,sps pps 数据可以作为一个普通h264帧,放在h264视频流的最前面。
        BOOL needSpsPps = NO;
        if (!encoder.spsPpsData) {
            if (isKeyFrame) {
                //获取avcC,这就是我们想要的sps和pps数据。
                //如果保存到文件中,需要将此数据前加上 [0 0 0 1] 4个字节,写入到h264文件的最前面。
                //如果推流,将此数据放入flv数据区即可。
                CMFormatDescriptionRef sampleBufFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
                NSDictionary *dict = (__bridge NSDictionary *)CMFormatDescriptionGetExtensions(sampleBufFormat);
                encoder.spsPpsData = dict[@"SampleDescriptionExtensionAtoms"][@"avcC"];
            }
            needSpsPps = YES;
        }
        
        //获取真正的视频帧数据
        CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        size_t blockDataLen;
        uint8_t *blockData;
        status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &blockDataLen, (char **)&blockData);
        if (status == noErr) {
            size_t currReadPos = 0;
            //一般情况下都是只有1帧,在最开始编码的时候有2帧,取最后一帧
            while (currReadPos < blockDataLen - 4) {
                uint32_t naluLen = 0;
                memcpy(&naluLen, blockData + currReadPos, 4);
                naluLen = CFSwapInt32BigToHost(naluLen);
                
                //naluData 即为一帧h264数据。
                //如果保存到文件中,需要将此数据前加上 [0 0 0 1] 4个字节,按顺序写入到h264文件中。
                //如果推流,需要将此数据前加上4个字节表示数据长度的数字,此数据需转为大端字节序。
                //关于大端和小端模式,请参考此网址:http://blog.csdn.net/hackbuteer1/article/details/7722667
                encoder.naluData = [NSData dataWithBytes:blockData + currReadPos + 4 length:naluLen];
                
                currReadPos += 4 + naluLen;
                
                encoder.isKeyFrame = isKeyFrame;
            }
        }else{
            [encoder onErrorWithCode:AWEncoderErrorCodeEncodeGetH264DataFailed des:@"got h264 data failed"];
        }
        
        ... ...
    }
    

    第四步,其实,此时硬编码已结束,这一步跟编码无关,将取得的h264数据,送到推流器中。

    -(aw_flv_video_tag *)encodeYUVDataToFlvTag:(NSData *)yuvData{
        
        ... ...
        
        if (status == noErr) {
            dispatch_semaphore_wait(self.vSemaphore, DISPATCH_TIME_FOREVER);
            if (_naluData) {
                //此处 硬编码成功,_naluData内的数据即为h264视频帧。
                //我们是推流,所以获取帧长度,转成大端字节序,放到数据的最前面
                uint32_t naluLen = (uint32_t)_naluData.length;
                //小端转大端。计算机内一般都是小端,而网络和文件中一般都是大端。大端转小端和小端转大端算法一样,就是字节序反转就行了。
                uint8_t naluLenArr[4] = {naluLen >> 24 & 0xff, naluLen >> 16 & 0xff, naluLen >> 8 & 0xff, naluLen & 0xff};
                //将数据拼在一起
                NSMutableData *mutableData = [NSMutableData dataWithBytes:naluLenArr length:4];
                [mutableData appendData:_naluData];
    
                //将h264数据合成flv tag,合成flvtag之后就可以直接发送到服务端了。后续会介绍
                aw_flv_video_tag *video_tag = aw_encoder_create_video_tag((int8_t *)mutableData.bytes, mutableData.length, ptsMs, 0, self.isKeyFrame);
                
                //到此,编码工作完成,清除状态。
                _naluData = nil;
                _isKeyFrame = NO;
                
                CVPixelBufferUnlockBaseAddress(pixelBuf, 0);
                
                CFRelease(pixelBuf);
                
                return video_tag;
            }
        }else{
            [self onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error"];
        }
        CVPixelBufferUnlockBaseAddress(pixelBuf, 0);
        
        CFRelease(pixelBuf);
        
        return NULL;
    

    第五步,关闭编码器

    //永远不忘记关闭释放资源。
    -(void)close{
        dispatch_semaphore_signal(self.vSemaphore);
        
        VTCompressionSessionInvalidate(_vEnSession);
        _vEnSession = nil;
        
        self.naluData = nil;
        self.isKeyFrame = NO;
        self.spsPpsData = nil;
    }
    

    硬编码AAC

    硬编码AAC逻辑同H264差不多。

    第一步,打开编码器

    -(void)open{
        //创建audio encode converter也就是AAC编码器
        //初始化一系列参数
        AudioStreamBasicDescription inputAudioDes = {
            .mFormatID = kAudioFormatLinearPCM,
            .mSampleRate = self.audioConfig.sampleRate,
            .mBitsPerChannel = (uint32_t)self.audioConfig.sampleSize,
            .mFramesPerPacket = 1,//每个包1帧
            .mBytesPerFrame = 2,//每帧2字节
            .mBytesPerPacket = 2,//每个包1帧也是2字节
            .mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount,//声道数,推流一般使用单声道
            //下面这个flags的设置参照此文:http://www.mamicode.com/info-detail-986202.html
            .mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsNonInterleaved,
            .mReserved = 0
        };
        
        //设置输出格式,声道数
        AudioStreamBasicDescription outputAudioDes = {
            .mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount,
            .mFormatID = kAudioFormatMPEG4AAC,
            0
        };
        
        //初始化_aConverter
        uint32_t outDesSize = sizeof(outputAudioDes);
        AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &outDesSize, &outputAudioDes);
        OSStatus status = AudioConverterNew(&inputAudioDes, &outputAudioDes, &_aConverter);
        if (status != noErr) {
            [self onErrorWithCode:AWEncoderErrorCodeCreateAudioConverterFailed des:@"硬编码AAC创建失败"];
        }
        
        //设置码率
        uint32_t aBitrate = (uint32_t)self.audioConfig.bitrate;
        uint32_t aBitrateSize = sizeof(aBitrate);
        status = AudioConverterSetProperty(_aConverter, kAudioConverterEncodeBitRate, aBitrateSize, &aBitrate);
        
        //查询最大输出
        uint32_t aMaxOutput = 0;
        uint32_t aMaxOutputSize = sizeof(aMaxOutput);
        AudioConverterGetProperty(_aConverter, kAudioConverterPropertyMaximumOutputPacketSize, &aMaxOutputSize, &aMaxOutput);
        self.aMaxOutputFrameSize = aMaxOutput;
        if (aMaxOutput == 0) {
            [self onErrorWithCode:AWEncoderErrorCodeAudioConverterGetMaxFrameSizeFailed des:@"AAC 获取最大frame size失败"];
        }
    }
    

    第二步,获取audio specific config,这是一个特别的flv tag,存储了使用的aac的一些关键数据,作为解析音频帧的基础。
    在rtmp中,必须将此帧在所有音频帧之前发送。

    -(aw_flv_audio_tag *)createAudioSpecificConfigFlvTag{
        //profile,表示使用的协议
        uint8_t profile = kMPEG4Object_AAC_LC;
        //采样率
        uint8_t sampleRate = 4;
        //channel信息
        uint8_t chanCfg = 1;
        //将上面3个信息拼在一起,成为2字节
        uint8_t config1 = (profile << 3) | ((sampleRate & 0xe) >> 1);
        uint8_t config2 = ((sampleRate & 0x1) << 7) | (chanCfg << 3);
        
        //将数据转成aw_data
        aw_data *config_data = NULL;
        data_writer.write_uint8(&config_data, config1);
        data_writer.write_uint8(&config_data, config2);
        
        //转成flv tag
        aw_flv_audio_tag *audio_specific_config_tag = aw_encoder_create_audio_specific_config_tag(config_data, &_faacConfig);
        
        free_aw_data(&config_data);
        
        //返回给调用方,准备发送
        return audio_specific_config_tag;
    }
    

    第三步:当从麦克风获取到音频数据时,将数据交给AAC编码器编码。

    -(aw_flv_audio_tag *)encodePCMDataToFlvTag:(NSData *)pcmData{
        self.curFramePcmData = pcmData;
        
        //构造输出结构体,编码器需要
        AudioBufferList outAudioBufferList = {0};
        outAudioBufferList.mNumberBuffers = 1;
        outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)self.audioConfig.channelCount;
        outAudioBufferList.mBuffers[0].mDataByteSize = self.aMaxOutputFrameSize;
        outAudioBufferList.mBuffers[0].mData = malloc(self.aMaxOutputFrameSize);
        
        uint32_t outputDataPacketSize = 1;
        
        //执行编码,此处需要传一个回调函数aacEncodeInputDataProc,以同步的方式,在回调中填充pcm数据。
        OSStatus status = AudioConverterFillComplexBuffer(_aConverter, aacEncodeInputDataProc, (__bridge void * _Nullable)(self), &outputDataPacketSize, &outAudioBufferList, NULL);
        if (status == noErr) {
            //编码成功,获取数据
            NSData *rawAAC = [NSData dataWithBytes: outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
            //时间戳(ms) = 1000 * 每秒采样数 / 采样率;
            self.manager.timestamp += 1024 * 1000 / self.audioConfig.sampleRate;
            //获取到aac数据,转成flv audio tag,发送给服务端。
            return aw_encoder_create_audio_tag((int8_t *)rawAAC.bytes, rawAAC.length, (uint32_t)self.manager.timestamp, &_faacConfig);
        }else{
            //编码错误
            [self onErrorWithCode:AWEncoderErrorCodeAudioEncoderFailed des:@"aac 编码错误"];
        }
        
        return NULL;
    }
    
    //回调函数,系统指定格式
    static OSStatus aacEncodeInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData){
        AWHWAACEncoder *hwAacEncoder = (__bridge AWHWAACEncoder *)inUserData;
        //将pcm数据交给编码器
        if (hwAacEncoder.curFramePcmData) {
            ioData->mBuffers[0].mData = (void *)hwAacEncoder.curFramePcmData.bytes;
            ioData->mBuffers[0].mDataByteSize = (uint32_t)hwAacEncoder.curFramePcmData.length;
            ioData->mNumberBuffers = 1;
            ioData->mBuffers[0].mNumberChannels = (uint32_t)hwAacEncoder.audioConfig.channelCount;
            
            return noErr;
        }
        
        return -1;
    }
    
    

    第四步:关闭编码器释放资源

    -(void)close{
        AudioConverterDispose(_aConverter);
        _aConverter = nil;
        self.curFramePcmData = nil;
        self.aMaxOutputFrameSize = 0;
    }
    

    文章列表

    1. 1小时学会:最简单的iOS直播推流(一)项目介绍
    2. 1小时学会:最简单的iOS直播推流(二)代码架构概述
    3. 1小时学会:最简单的iOS直播推流(三)使用系统接口捕获音视频
    4. 1小时学会:最简单的iOS直播推流(四)如何使用GPUImage,如何美颜
    5. 1小时学会:最简单的iOS直播推流(五)yuv、pcm数据的介绍和获取
    6. 1小时学会:最简单的iOS直播推流(六)h264、aac、flv介绍
    7. 1小时学会:最简单的iOS直播推流(七)h264/aac 硬编码
    8. 1小时学会:最简单的iOS直播推流(八)h264/aac 软编码
    9. 1小时学会:最简单的iOS直播推流(九)flv 编码与音视频时间戳同步
    10. 1小时学会:最简单的iOS直播推流(十)librtmp使用介绍
    11. 1小时学会:最简单的iOS直播推流(十一)sps&pps和AudioSpecificConfig介绍(完结)

    相关文章

      网友评论

      • 帅聪哥:感谢大神的分享,可以说是我看到直播推流这块最详细的了。表示膜拜
      • 纠结的哈士奇:有个疑问想请教下:我这边有个mp4视频,视频是h.264编码,ios这边我用applayer播放黑屏,不知道楼主知道这个原因么?

        还有是不是我得264解码后,才能正常播放呢?
      • 超大号的空瓶子:@hard_man 等候9、10、11、12 的更新,写的非常不错!👍时刻关注中
      • 新地球说着一口陌生腔调:硬编码 具体是什么硬件处理的呢
        joymake:显卡gpu
      • 9284523c8bea:大神等你后续文章
      • yX_:大神,编码之后的推流部分的文章有写吗,是在哪里的,为什么后面的几篇文章没有链接打开,而且我下载了你的demo也运行不了
      • Liusr:何时更新下nginx 和 rtmp的配置

      本文标题:1小时学会:最简单的iOS直播推流(七)h264/aac 硬编码

      本文链接:https://www.haomeiwen.com/subject/uwjjmttx.html