iOS video h.264 hardcoded in review - code reduction

The implementation of this code example: The iOS side needs to realize the capture of h264 encoding and write to the local file under the document

Camera preparation, encoding preparation

1. Start camera avatar capture

- (void)startCapture
{
    //1. 相机相关初始化及配置
    self.captureSession = [[AVCaptureSession alloc]init];
    
    self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
    
    cCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    cEncodeQueue  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    AVCaptureDevice *inputCamera = nil;
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in devices) {

        if ([device position] == AVCaptureDevicePositionBack) {
            
            inputCamera = device;
        }
    }
    
    self.cCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil];
    
    if ([self.captureSession canAddInput:self.cCaptureDeviceInput]) {
        
        [self.captureSession addInput:self.cCaptureDeviceInput];
    }
    
    //2. 相机输出设置
    self.cCaptureDataOutput = [[AVCaptureVideoDataOutput alloc]init];

    [self.cCaptureDataOutput setAlwaysDiscardsLateVideoFrames:NO];
    
    [self.cCaptureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    
    [self.cCaptureDataOutput setSampleBufferDelegate:self queue:cCaptureQueue];
    
    if ([self.captureSession canAddOutput:self.cCaptureDataOutput]) {
        
        [self.captureSession addOutput:self.cCaptureDataOutput];
    }
    
    //3. 输出连接AVCaptureConnection 捕获会话中特定捕获输入对和捕获输出对象之间的连接。

    AVCaptureConnection *connection = [self.cCaptureDataOutput connectionWithMediaType:AVMediaTypeVideo];
    
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    
    //4. 拍摄画面预览
    self.cPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];

    [self.cPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
    
    [self.cPreviewLayer setFrame:self.view.bounds];
    
    [self.view.layer addSublayer:self.cPreviewLayer];
    
    //5. 设置即将写入到的目录文件
    NSString *filePath = [NSHomeDirectory()stringByAppendingPathComponent:@"/Documents/video.h264"];

    [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
    
    BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
    if (!createFile) {
        
        NSLog(@"create file failed");
    }else
    {
        NSLog(@"create file success");

    }
    
    NSLog(@"filePaht = %@",filePath);
    fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
    
    
    //6. 初始化videoToolbBox
    [self initVideoToolBox];
    
    //7. 开始捕捉
    [self.captureSession startRunning];
}

//停止捕捉
- (void)stopCapture
{   
    [self.captureSession stopRunning];
    
    [self.cPreviewLayer removeFromSuperlayer];
    
    [self endVideoToolBox];
    
    [fileHandle closeFile];
    
    fileHandle = NULL;
}
复制代码

2. Initialize videoToolBox

-(void)initVideoToolBox
{
    dispatch_sync(cEncodeQueue, ^{
        frameID = 0;
        
        int width = 480, height = 640;
        
         //    第一步:创建编码会话
        /*
           参数
         1. NULL 默认分配器
         2. width
         3. height  分辨率 像素为单位
         4. 编码类型
         5. NULL 编码规范, videoToolBox自动选择
         6. NULL 源像素缓存区
         7. NULL 默认分配压缩数据分配器
         8. 回调函数:函数指针 指向函数名 didCompressH264
         9. 桥接self
         10.编码会话变量
         */
        OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void * _Nullable)(self), &cEncodeingSession);
        
        NSLog(@"H264:VTCompressionSessionCreate %d", (int)status);
        
        // 0 :  noErr = 0
        if (status != noErr) {
            NSLog(@"H264:unable to creat H264 Session");
            return;
        }
        /*
         第二步:设置编码参数
         */
        //1.1.设置实时编码参数、实时编码输出(避免延迟)
        VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        //1.2.设置 ProfileLevel
        VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
        //1.3.设置关键
        int frameInterval = 10;
        CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);//类型转换 int ->  CFNumberRef
        VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
        
        //1.4. 设置期望帧率,不是实际帧率,设置关键帧GOPSize间隔,gop太小会模糊
        int fps = 10;
        CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
        VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

        //1.5. 设置码率上限,单位bps
        int bitRate = width * height * 3 * 4 * 8; //码率计算公式 极高码率 (width * height * 3)* 4 ,
        //这里再 * 8 或者 *10 都可以
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bitRate);
        VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);

        //第二步 准备编码
        VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
        
        //
        //AVFoundation捕捉的时候才有数据, 编码
        //这里就来到 输出cmsamplebuffer里
    });

}
//结束VideoToolBox
-(void)endVideoToolBox
{
    VTCompressionSessionCompleteFrames(cEncodeingSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(cEncodeingSession);
    CFRelease(cEncodeingSession);
    cEncodeingSession = NULL;
}

复制代码

The above two steps of camera preparation and encoding preparation are basically completed, and the next step is to receive samplebuffer and encode

3. Receive samplebuffer for encoding

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    //区分音/视频  
    // 可以根据 captureOutput 是 videocaptureOutput 还是 audiocaptureOutput
    dispatch_sync(cEncodeQueue, ^{
        [self encode:sampleBuffer];
    });
    
}

//编码
- (void) encode:(CMSampleBufferRef )sampleBuffer
{
    //拿到每一帧未编码的数据
    //视频中1帧数据
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    //设置帧时间,如果不设置导致时间轴过长
    CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
    
    VTEncodeInfoFlags flags;//
    
    //第三步:开始编码
    /*
     参数:
     1.编码会话变量
     2.未编码数据
     3.获取到的sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳
     4.对于获取到sample buffer数据,这个帧的展示时间。如果没有时间信息,可设置KCMTimeInvalid
     5.frameProperties 包含这个帧的属性. 帧的改变会影响后边的编码帧
     6. ourceFrameRefCom 回调函数会引用你设置的这个帧的参考值
     7.infoFlagsOut 指向一个VTEncodeInfoFlags来接受一个编码操作,如果使用异步运行,KVTEncodeInfo_Asynchronous被设置;同步运行,KVTEncodeInfo_FrameDropped被设置;设置NULL为不想接收这个信息
     */
    OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
    
    if (statusCode != noErr) {
        //error
        NSLog(@"H264:VTCompressionSessionEncodeFrame fail with %d", statusCode);
        //释放
        VTCompressionSessionInvalidate(cEncodeingSession);
        CFRelease(cEncodeingSession);
        cEncodeingSession = NULL;
        return;
    }
    NSLog(@"H264:VTCompressionSessionEncodeFrame Success");
    
    //完成编码
}



复制代码

4. Write to file after encoding

    // h264 file format sps+pps+stream data

    //Sequence parameter set sps (Sequence Parameter Sets)

    //Image parameter set pps (Picture Parameter Sets)

    // 00 00 00 01 separator

//编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{    
    if (status != 0) { // 0 / noError
        return;
    }
    //没准备好
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"H264: CMSampleBufferDataIsReady is not");
        return;
    }
    // encoder: 桥接过来的self
    ViewController *encoder = (__bridge ViewController *)(outputCallbackRefCon);
    
    //判断当前是否为关键帧
    bool keyFrame = CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
    
    //判断当前帧是否为关键帧
    //获取sps & pps 数据 只获取1次,保存在h264文件的第一帧中
    //sps(sample per second 采样次数/s ),是衡量模数转换(ADC)时采样速率的单位
    //pps()
    if (keyFrame) {
        
        //图像存储方式,编码器等格式描述(获取原图像存储格式)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        //sps
        size_t sparameterSetSize, sparameterSetCount;   //数据大小 数据个数
        const uint8_t *sparameterSet;       //数据内容指针
        //获取sps
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
        
        if (statusCode == noErr) {
            
            //pps
            size_t pparameterSetSize, pparameterSetCount;   //数据大小 数据个数
            const uint8_t *pparameterSet;       //数据内容指针
            //获取sps
            OSStatus pStatusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);

            if (pStatusCode == noErr) {
                
                // C语言的数据 转为 oc的NSData
                //获取sps data
                NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                //获取pps data
                NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                
                if (encoder) {
                    //将 sps 和pps 的头写入file
                    [encoder gotSpsPps:sps pps:pps];
                }
                
            }
            
            
        }
    }
    //前面头已经写入
    // 接下来写其他数据
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    
    //数据的  总长度,单个数据长度,存储地址
    size_t length, totalLength;
    char *dataPointer;
    //获取 数据信息
    OSStatus bStatusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    
    
    //大端模式 转  小端模式
    /*
     大端模式
     高字节的数据在前面,低字节的数据在后面
     12 34 大端
     */
    /*
     小端模式
     低字节的数据在前面,高字节的数据在后面
     34 12 小端
     */
    if (bStatusCodeRet == noErr) {
        size_t bufferOffset = 0;    //数据偏移量
        static const int AVCCHeaderLength = 4;  //
        //返回的nalu数据前4个字节不是001的startCode,而是大端模式的帧长度length
        
        //循环获取nalu数据
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            //当还没偏移完时
            
            uint32_t NALUnitLength = 0;
            //读/获取 1单元的 nalu 数据
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            //从大端模式 转换为 系统端模式
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            //获取nalu数据 流数据
            NSData *data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            
            //将nalu写入文件
            [encoder gotEncodedData:data isKeyFrame:keyFrame];
            
            //移到下一个NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
        
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    //
    NSLog(@"gotSpsPps length: %d pps: %d", (int)[sps length], (int)[pps length]);
    
    const char bytes[] = "\x00\x00\x00\x01";
    
    //为什么 -1 , 在 C语言中 字符处 以\0结束,"\x00\x00\x00\x01\0";  在计算的时候 -1
    size_t length = sizeof(bytes) - 1;
//                    (sizeof bytes);
    
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    [fileHandle writeData:ByteHeader];  //4
    [fileHandle writeData:sps];
    [fileHandle writeData:ByteHeader];  //间隔
    [fileHandle writeData:pps]; 
}


- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData length: %d ", (int)[data length]);
    
    const char bytes[] = "\x00\x00\x00\x01";
    
    //长度
    size_t length = sizeof(bytes) - 1;
                    
    //头字节
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    //写入头字节
    [fileHandle writeData:ByteHeader];  //4
    //写入h264数据
    [fileHandle writeData:data];
}




复制代码

Guess you like

Origin juejin.im/post/7085979024261906462