AVFoundation-04视频捕捉编码

概述

AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。

iOS 媒体环境.png

捕捉会话

AV Foundation 捕捉栈的核心类是AVCaptureSession。一个捕捉会话相当于一个虚拟的插线板,用于连接输入和输出的资源。捕捉会话管理从物理设备得到的数据流。捕捉会话可以额外配置一个会话预设(session preset),用于控制数据的捕捉和质量,会话的预设值为AVCaptureSessionPresetHigh。

self.captureSession = [[AVCaptureSession alloc] init];
[self.captureSession setSessionPreset:AVCaptureSessionPreset640x480];

捕捉设备

AVCaptureDevice为诸如摄像头或麦克风等物理设备等提供了一个接口。最常用的是+ (nullable AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType;,根据指定的媒体类型返回系统默认的设备。

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
    return;
}
[self.captureSession addInput:input];

捕捉输入输出

在使用捕捉设备进行处理前,首先要添加输入、输出设备。通常情况下AVCaptureDevice不能直接添加到AVCaptureSession中,我们需要使用AVCaptureDeviceInput。AVCaptureOutput是一个抽象基类,用于将捕捉到的数据输出。框架定义了AVCaptureOutput的一些扩展,具体如下所示:

类型 说明
AVCaptureStillImageOutput 图像输出,iOS10.0版本废弃,使用AVCapturePhotoOutput替代
AVCapturePhotoOutput 图像输出,iOS10.0版本引入
AVCaptureVideoDataOutput 视频输出
AVCaptureAudioDataOutput 音频输出
AVCaptureFileOutput 音视频文件输出,子类 AVCaptureMovieFileOutput、AVCaptureAudioFileOutput
AVCaptureMetadataOutput 元数据输出

捕捉预览

AVCaptureVideoPreviewLayer是CALayer的子类,可以对捕捉视频进行实时预览。它有个AVLayerVideoGravity属性可以控制画面的缩放和拉升效果。

AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
previewLayer.frame = [UIScreen mainScreen].bounds;
[self.view.layer addSublayer:previewLayer];

H264编码

H264码流文件分为两层:
(1) VCL(Video Coding Layer)视频编码层:负责高效的视频内容表示,VCL 数据即编码处理的输出,它表示被压缩编码后的视频数据序列。
(2) NAL(Network Abstraction Layer)网络提取层:负责以网络所要求的恰当的方式对数据进行打包和传送,是传输层,不管是在本地播放还是在网络播放的传输,都要通过这一层来传输。

在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。以下是三种帧的说明:

I 帧

I帧:帧内编码帧 ,I 帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)。I 帧特点:

  1. 它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;
  2. 解码时仅用I帧的数据就可重构完整图像;
  3. I帧描述了图像背景和运动主体的详情;
  4. I帧不需要参考其他画面而生成;
  5. I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
  6. I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;
  7. I帧不需要考虑运动矢量;
  8. I帧所占数据的信息量比较大。

P 帧

P 帧:前向预测编码帧。P 帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P 帧没有完整画面数据,只有与前一帧的画面差别的数据)P 帧的预测与重构:P 帧是以I帧为参考帧,在I帧中找出P 帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到 P 帧“某点”样值,从而可得到完整的P 帧。P 帧特点:

  1. P帧是I帧后面相隔1~2帧的编码帧;
  2. P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差);
  3. 解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;
  4. P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;
  5. P帧可以是其后面P帧的参考帧,也可以是其前后的B帧的参考帧;
  6. 由于P帧是参考帧,它可能造成解码错误的扩散;
  7. 由于是差值传送,P帧的压缩比较高。

B帧

B帧:双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。B帧特点:

1)B帧是由前面的I或P帧和后面的P帧来进行预测的;
2)B帧传送的是它与前面的I或P帧和后面的P帧之间的预测误差及运动矢量;
3)B帧是双向预测编码帧;
4)B帧压缩比最高,因为它只反映两参考帧间运动主体的变化情况,预测比较准确;
5)B帧不是参考帧,不会造成解码错误的扩散。

NAL Units

在 VCL 数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进NAL 单元中。每个 NAL 单元包括一个原始字节序列负荷( RBSP, Raw Byte Sequence Payload)和一组对应于视频编码的 NAL 头信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾比特。一个 bit“1”若干比特“0”,以便字节对齐。

NAL Units

NAL头由一个字节组成 ,如图2所示。
语法:禁止位(1bit)、重要性位(2bit)、NALU类型(5bit)。

图片 1.png

NAL头信息的每一位说明如下

简称 全称 中文 作用
0 F forbidden_zero_bit 禁止位 网络发现NAL单元有比特错误时可设置该比特为1,以便接收方丢掉该单元
1-2 NRI nal_ref_idc 重要性指示位 标志该NAL单元用于重建时的重要性,值越大,越重要。取 00 ~ 11
3-7 TYPE nal_unit_type NALU类型 1 ~ 23表示单个NAL包,24 ~ 31需要分包或者组合发送,具体含义需要参考下面的表格

常见的NAL类型

NAL类型 描述
1 不分区,非IDR图像的片
2 片分区A
3 片分区B
4 片分区C
5 IDR图像中的片
6 补充增强信息单元(SEI)
7 SPS(Sequence Parameter Set序列参数集,作用于一串连续的视频图像,即视频序列)
8 PPS(Picture Parameter Set图像参数集,作用于视频序列中的一个或多个图像)
9 序列结束
10 序列结束
11 码流结束
12 填充

Profile

H.264有四种画质级别,分别是BP、EP、MP、HP:

Profile 描述
BP(Baseline profile) 提供I/P帧,仅支持Progressive(逐行扫描)和CAVLC。多应用于“视频会话”,如可视电话、会议电视、远程教学、视频监控等实时通信领域;
EP(Extended profile) 提供I/P/B/SP/SI帧,仅支持Progressive和CAVLC。多应用于流媒体领域,如视频点播、基于网络的视频监控等;
MP(Main profile) 提供I/P/B帧,支持Progressive和Interlaced(隔行扫描),提供CAVLC和CABAC。多应用于数字电视广播、数字视频存储等领域;
HiP(High profile) (Fidelity Range Extensions,FRExt) 在Main profile基础上新增8*8帧内预测,Custom Quant,Lossless Video Coding,更多YUV格式(4:2:2,4:4:4),像素精度提高到10位或14位。多应用于对高分辨率和高清晰度有特别要求的领域。

颜色空间

Profile 描述
X264_CSP_I420 yuv 4:2:0 planar
X264_CSP_YV12 yvu 4:2:0 planar
X264_CSP_NV12 yuv 4:2:0, with one y plane and one packed u+v
X264_CSP_NV21 yuv 4:2:0, with one y plane and one packed v+u
X264_CSP_I422 yuv 4:2:2 planar
X264_CSP_YV16 yvu 4:2:2 planar
X264_CSP_NV16 yuv 4:2:2, with one y plane and one packed u+v
X264_CSP_V210 10-bit yuv 4:2:2 packed in 32
X264_CSP_I444 yuv 4:4:4 planar
X264_CSP_YV24 yvu 4:4:4 planar
X264_CSP_BGR packed bgr 24bits
X264_CSP_BGRA packed bgr 32bits
X264_CSP_RGB packed rgb 24bits

视频码率

视频码率是视频数据(视频色彩量、亮度量、像素量)每秒输出的位数。一般用的单位是kbps。
在视频会议应用中,视频质量和网络带宽占用是矛盾的,通常情况下视频流占用的带宽越高则视频质量也越高;如要求高质量的视频效果,那么需要的网络带宽也越大;解决这一矛盾的钥匙当然是视频编解码技术。评判一种视频编解码技术的优劣,是比较在相同的带宽条件下,哪个视频质量更好;在相同的视频质量条件下,哪个占用的网络带宽更少。

一套设置码率的公式

项目 计算公式 192x144 320x240 480x360 640x480 1280x720 1920x1080
极低码率 (宽x高x3)/4 30kb/s 60kb/s 120kps 250kbps 500kbps 1mbps
低码率 (宽x高x3)/2 60kb/s 120kb/s 250kbps 500kbps 1mbps 2mbps
中码率 (宽x高x3) 120kb/s 250kb/s 500kbps 1mbps 2mbps 4mbps
高码率 (宽x高x3)x 2 250kb/s 500kb/s 1mbps 2mbps 4mbps 8mps
极高码率 (宽x高x3)x4 500kb/s 1mb/s 2mbps 4mbps 8mbps 16mbps

硬编码

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

  • 初始化编码器。
- (void)setupEncoder:(int)width height:(int)height
{
    _videoQueue = dispatch_queue_create("com.qm.video.encode", NULL);
    
    dispatch_sync(_videoQueue, ^{
        
        // 编码类型:kCMVideoCodecType_H264
        // 编码回调:didCompressH264,这个回调函数为编码结果回调,编码成功后,会将数据传入此回调中。
        OSStatus status = VTCompressionSessionCreate(NULL,
                                                     width,
                                                     height,
                                                     kCMVideoCodecType_H264,
                                                     NULL,
                                                     NULL,
                                                     NULL,
                                                     didCompressH264,
                                                     (__bridge void *)(self),
                                                     &EncodingSession);
        
        NSLog(@"H264: VTCompressionSessionCreate %d", (int)status);
        
        if (status != noErr)
        {
            NSLog(@"H264: Unable to create a H264 session");
            return ;
            
        }
        
        // 设置实时编码
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        
        // ProfileLevel,h264的协议等级,不同的清晰度使用不同的ProfileLevel。
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
        
        // 设置码率
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(width * height * 3));
        
        // 关闭重排Frame,因为有了B帧(双向预测帧,根据前后的图像计算出本帧)后,编码顺序可能跟显示顺序不同。此参数可以关闭B帧
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
        
        // 关键帧最大间隔,关键帧也就是I帧。此处表示关键帧最大间隔为2s。
        VTSessionSetProperty(EncodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(25 * 2));
        
        // Tell the encoder to start encoding
        VTCompressionSessionPrepareToEncodeFrames(EncodingSession);
    });
}
  • 硬编码NV12数据。
- (void)encodeBuffer:(CMSampleBufferRef )sampleBuffer
{
    dispatch_sync(_videoQueue, ^{
        
        _frameCount++;
        // 获取 CVImageBufferRef
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        
        // 设置 pts
        CMTime presentationTimeStamp = CMTimeMake(_frameCount, 1000);
        
        VTEncodeInfoFlags flags;
        // 编码
        OSStatus statusCode = VTCompressionSessionEncodeFrame(EncodingSession,
                                                              imageBuffer,
                                                              presentationTimeStamp,
                                                              kCMTimeInvalid,
                                                              NULL, NULL, &flags);
        // 错误处理
        if (statusCode != noErr) {
            NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
            
            // End the session
            VTCompressionSessionInvalidate(EncodingSession);
            CFRelease(EncodingSession);
            EncodingSession = NULL;
            return;
        }
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    });
    
}
  • 处理编码回调。
static void didCompressH264(void *outputCallbackRefCon,
                            void *sourceFrameRefCon,
                            OSStatus status,
                            VTEncodeInfoFlags infoFlags,
                            CMSampleBufferRef sampleBuffer )
{
    NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
    if (status != 0) return;
    
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
        NSLog(@"didCompressH264 data is not ready ");
        return;
    }
    H264HwEncoder* encoder = (__bridge H264HwEncoder*)outputCallbackRefCon;
    
    // 检查是否为关键帧
    bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    if (keyframe)
    {
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        if (statusCode == noErr)
        {
            // 获取 sps
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
            if (statusCode == noErr)
            {
                // 获取 pps
                NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                if (encoder->_delegate)
                {
                    [encoder->_delegate gotSpsPps:sps pps:pps starCode:NO];
                }
            }
        }
    }
    
    // 获取其它视频帧
    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;
        // 一般情况下都是只有1帧,在最开始编码的时候有2帧,取最后一帧
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            
            // 获取 NAL unit 数据长度
            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];
            
            // 如果保存到文件中,需要将此数据前加上 [0 0 0 1] 4个字节,按顺序写入到h264文件中。
            [encoder->_delegate gotEncodedData:data isKeyFrame:keyframe starCode:NO];
            
            // 转到下一个 NAL unit
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
        
    }
    
}
  • 释放资源。
- (void)destroy
{
    // Mark the completion
    VTCompressionSessionCompleteFrames(EncodingSession, kCMTimeInvalid);
    
    // End the session
    VTCompressionSessionInvalidate(EncodingSession);
    CFRelease(EncodingSession);
    EncodingSession = NULL;
}

软编码

软编码是指通过CPU计算进行数据编码,主要计算操作在CPU中。软编码的特点是,灵活,多样,功能丰富可扩展,但是CPU占用较多。在这里软编码使用的是 x264 http://www.videolan.org/developers/x264.html

  • 初始化编码器。
- (BOOL)setupEncoderWithWidth:(int)width height:(int)height frameRate:(int)fps bitrate:(int)bitrate
{
    _width = width;
    _height = height;
    
    x264_param_t param;
    //x264_param_default_preset 设置
    x264_param_default_preset(&param,"ultrafast","zerolatency");
    //编码输入的像素格式YUV420P
    param.i_csp = X264_CSP_NV12;
    param.i_width  = width;
    param.i_height = height;
    
    //参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
    //恒定码率,会尽量控制在固定码率
    param.rc.i_rc_method = X264_RC_CRF;
    param.rc.i_bitrate = bitrate / 1000; //* 码率(比特率,单位Kbps)
    param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2; //瞬时最大码率
    
    //码率控制不通过timebase和timestamp,而是fps
    param.b_vfr_input = 0;
    param.i_fps_num = fps; //* 帧率分子
    param.i_fps_den = 1; //* 帧率分母
    param.i_timebase_den = param.i_fps_num;
    param.i_timebase_num = param.i_fps_den;
    param.i_threads = 1;//并行编码线程数量,0默认为多线程
    
    //是否把SPS和PPS放入每一个关键帧
    //SPS Sequence Parameter Set 序列参数集,PPS Picture Parameter Set 图像参数集
    //为了提高图像的纠错能力
    param.b_repeat_headers = 1;
    //设置Level级别
    param.i_level_idc = 51;
    //设置Profile档次
    //baseline级别,没有B帧
    x264_param_apply_profile(&param,"baseline");
    
    _x264Handle = x264_encoder_open(&param);
    
    if(_x264Handle == NULL) {
        return NO;
    }
    
    _pPicIn = malloc(sizeof(x264_picture_t));
    x264_picture_alloc(_pPicIn, X264_CSP_NV12, _width, _height);
    _pPicIn->img.i_csp = X264_CSP_NV12;
    _pPicIn->img.i_plane = 2;
    
    _pPicOut = malloc(sizeof(x264_picture_t));
    x264_picture_init(_pPicOut);
    
    return YES;
}
  • 编码NV12数据。
- (void)encodeBuffer:(CMSampleBufferRef)sampler
{
    CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampler);
  
    //表示开始操作数据
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    
    //Y数据
    uint8_t *yFrame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
    
    // UV数据
    uint8_t *uvFrame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
    
    //* 编码需要的辅助变量
    int iNal = 0;
    x264_nal_t *pNals = NULL;
    
    _pPicIn->img.i_csp = X264_CSP_NV12;
    _pPicIn->img.i_plane = 2;
   
    _pPicIn->img.plane[0] = yFrame;
    _pPicIn->img.plane[1] = uvFrame;
    
    int frame_size = x264_encoder_encode(_x264Handle,
                                         &pNals,
                                         &iNal,
                                         _pPicIn,
                                         _pPicOut);
    
    _pPicIn->i_pts += 1;
    _pPicIn->i_dts += 1;
    
    if(frame_size > 0) {
        for (int i = 0; i < iNal; i++) {
            NSLog(@"%d", pNals[i].i_type);
            
            // SPS
            if (pNals[i].i_type == NAL_SPS) {
                NSData *sps = [NSData dataWithBytes:pNals[i].p_payload length:pNals[i].i_payload];
                [_delegate gotSpsPps:sps pps:nil starCode:YES];
                
            // PPS
            }else if(pNals[i].i_type == NAL_PPS) {
                NSData *pps = [NSData dataWithBytes:pNals[i].p_payload length:pNals[i].i_payload];
                [_delegate gotSpsPps:nil pps:pps starCode:YES];
                
            }else {
                NSData *data = [NSData dataWithBytes:pNals[i].p_payload length:pNals[i].i_payload];
                [_delegate gotEncodedData:data isKeyFrame:NO starCode:YES];
            }
        }
    }
 
    // Unlock
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
}
  • 释放资源
- (void)destroy
{
    //* 清除图像区域
    x264_picture_clean(_pPicIn);
    x264_picture_clean(_pPicOut);
    
    //* 关闭编码器句柄
    x264_encoder_close(_x264Handle);
    _x264Handle = NULL;
}

使用编码器

分别使用软编码、硬编码编码NV12视频数据,将编码的h264文件保存到沙盒的Document目录中,然后用VLC播放器可以直接播放。注意如果保存到文件中,需要将此数据前加上 [0x00 0x00 0x00 0x01] 4个字节,按顺序写入到h264文件中,但是软编码已经加了[0x00 0x00 0x00 0x01],因此不用主动添加。

//
//  ViewController.m
//  Movie
//
//  Created by qinmin on 2017/6/30.
//  Copyright © 2017年 qinmin. All rights reserved.
//

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "H264HwEncoder.h"
#import "X264Utils.h"


#define kDocumentPath(path) [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:path]

@interface ViewController () <AVCaptureVideoDataOutputSampleBufferDelegate,H264HwEncoderDelegate,X264Delegate>
@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoOutput;

@property (nonatomic, strong) H264HwEncoder *h264HwEncoder;
@property (nonatomic, strong) X264Utils *x264Encoder;

@property (nonatomic, assign) FILE *fileHandle;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setupEncoder1];
    [self setupOutputFile];
    [self setupCaptureSession];
}

- (void)setupOutputFile
{
    [[NSFileManager defaultManager] removeItemAtPath:kDocumentPath(@"output.h264") error:nil];
    _fileHandle = fopen([kDocumentPath(@"output.h264") UTF8String], "ab+");
}

- (void)setupEncoder
{
    self.h264HwEncoder = [[H264HwEncoder alloc] init];
    [self.h264HwEncoder setupEncoder:640 height:480];
    [self.h264HwEncoder setDelegate:self];
}

- (void)setupEncoder1
{
    self.x264Encoder = [[X264Utils alloc] init];
    [self.x264Encoder setupEncoderWithWidth:640 height:480 frameRate:25 bitrate:640*480*3];
    [self.x264Encoder setDelegate:self];
}

- (void)setupCaptureSession
{
    self.captureSession = [[AVCaptureSession alloc] init];
    [self.captureSession setSessionPreset:AVCaptureSessionPreset640x480];
    
    // Create a device input with the device and add it to the session.
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error;
    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
    if (!input) {
        return;
    }
    [self.captureSession addInput:input];
    
    // Create a VideoDataOutput and add it to the session
    _videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    [_videoOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(0, 0)];
    _videoOutput.videoSettings = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
    [self.captureSession addOutput:_videoOutput];
    
    AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
    previewLayer.frame = [UIScreen mainScreen].bounds;
    [self.view.layer addSublayer:previewLayer];
    
    // Start the session running to start the flow of data
    [self.captureSession startRunning];
}

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection
{
    if (!self.captureSession.isRunning) {
        return;
    }else if (captureOutput == _videoOutput) {
        //CVImageBufferRef cameraFrame = CMSampleBufferGetImageBuffer(sampleBuffer);
        //int bufferWidth = (int) CVPixelBufferGetWidth(cameraFrame);
        //int bufferHeight = (int) CVPixelBufferGetHeight(cameraFrame);
    
        [self.h264HwEncoder encodeBuffer:sampleBuffer];
        [self.x264Encoder encodeBuffer:sampleBuffer];
    }
}

- (void)gotSpsPps:(NSData *)sps pps:(NSData *)pps starCode:(BOOL)flag
{
    char slide[] = "\x00\x00\x00\x01";
    if (sps) {
        if (!flag) {
            fwrite(slide, 1, 4, _fileHandle);
        }
        fwrite(sps.bytes, 1, sps.length, _fileHandle);
    }
    
    if (pps) {
        if (!flag) {
            fwrite(slide, 1, 4, _fileHandle);
        }
        fwrite(pps.bytes, 1, pps.length, _fileHandle);
    }
}

- (void)gotEncodedData:(NSData *)data isKeyFrame:(BOOL)isKeyFrame starCode:(BOOL)flag
{
    if (!flag) {
        char slide[] = "\x00\x00\x00\x01";
        fwrite(slide, 1, 4, _fileHandle);
    }
    
    fwrite(data.bytes, 1, data.length, _fileHandle);
}

@end

参考

AVFoundation开发秘籍:实践掌握iOS & OSX应用的视听处理技术

源码地址:AVFoundation开发 https://github.com/QinminiOS/AVFoundation

最后

如果需要自己渲染视频视频可以参考之前的文章:
OpenGL ES入门11-相机视频渲染
GPUImage源码阅读(五)

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

推荐阅读更多精彩内容