iOS-視屏編碼

編碼方式

  • 在iOS中編碼方式有兩種

    • 硬編碼: 在iOS8.0之后,使用原生框架VideoToolBox&AudioToolbox對視屏和音頻進行硬編碼.
    • 軟編碼: 使用CPU進行編碼,通常使用的框架為ffmpeg+x264.
      • ffmpeg:是一套開源的框架, 用于對音視頻進行編碼&解碼&轉化計算機程序
      • x264x264是一種免費的、開源的、具有更優秀算法的H.264/MPEG-4 AVC視頻壓縮編碼方式.
  • 編碼方式對比:

    • 硬編碼性能高于軟編碼,對CPU無要求和壓力,對硬件要求較高(如GPU
    • 軟編碼對CPU的負載較高,容易造成手機發熱,但其實現簡單,編碼自由度高(可以自由調整想要的參數)

硬編碼

編碼流程:采集視屏信息--> 獲取到視頻幀--> 對視頻幀進行編碼 --> 獲取到視頻幀信息 --> 將編碼后的數據以NALU方式寫入到文件

  • 采集視屏信息

    - (void)startCapture:(UIView *)preview{
       // 1.創建捕捉會話
       AVCaptureSession *session = [[AVCaptureSession alloc] init];
       session.sessionPreset = AVCaptureSessionPreset1280x720;
       self.captureSession = session;
       
       // 2.設置輸入設備
       AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
       NSError *error = nil;
       AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
       [session addInput:input];
       
       // 3.添加輸出設備
       AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
       self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
       [output setSampleBufferDelegate:self queue:self.captureQueue];
       [session addOutput:output];
       
       // 設置錄制視頻的方向
       AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
       [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
       
       // 4.添加預覽圖層
       AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
       previewLayer.frame = preview.bounds;
       [preview.layer insertSublayer:previewLayer atIndex:0];
       self.previewLayer = previewLayer;
       
       // 5.開始捕捉
       [self.captureSession startRunning];
    }
    
  • 獲取到視屏幀

     - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
        // 對sampleBuffer進行硬編碼處理
       [self.encoder encodeSampleBuffer:sampleBuffer];
    }   
    
  • 對視屏幀進行硬編碼
    (1). 初始化寫文件對象,用于保存編碼完的視屏信息

    - (void)setupFileHandle {
        // 1.獲取沙盒路徑
        NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"video.h264"];
        
        // 2.如果原來有文件,則刪除
        [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
        [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
        
        // 3.創建對象
        self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
    }
    

    (2). 初始化編碼會話

    通過VTSessionSetProperty設置對象屬性:

    • 編碼方式:H.264編碼
    • 幀率:每秒鐘多少幀畫面
    • 碼率:單位時間內保存的數據量
    • 關鍵幀(GOPsize)間隔:多少幀為一個GOP
    - (void)setupVideoSession {
        // 1.用于記錄當前是第幾幀數據(畫面幀數非常多)
        self.frameID = 0;
        
        // 2.錄制視頻的寬度&高度
        int width = [UIScreen mainScreen].bounds.size.width;
        int height = [UIScreen mainScreen].bounds.size.height;
        
        // 3.創建CompressionSession對象,該對象用于對畫面進行編碼
        // kCMVideoCodecType_H264 : 表示使用h.264進行編碼
        // didCompressH264 : 當一次編碼結束會在該函數進行回調,可以在該函數中將數據,寫入文件中
        VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);
        
        // 4.設置實時編碼輸出(直播必然是實時輸出,否則會有延遲)
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        
        // 5.設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)
        int fps = 30;
        CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
        
        // 6.設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 如果碼率較低會引起馬賽克 --> 碼率高有利于還原原始畫面,但是也不利于傳輸)
        int bitRate = 800*1024;
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
        NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
        
        // 7.設置關鍵幀(GOPsize)間隔
        int frameInterval = 30;
        CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
        
        // 8.基本設置結束, 準備進行編碼  
        VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
      }
    

    (3). 開始編碼

      - (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
      // 1.將sampleBuffer轉成imageBuffer
      CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
      
      // 2.根據當前的幀數,創建CMTime的時間
      CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
      VTEncodeInfoFlags flags;
      
      // 3.開始編碼該幀數據
      OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                            imageBuffer,
                                                            presentationTimeStamp,
                                                            kCMTimeInvalid,
                                                            NULL, (__bridge void * _Nullable)(self), &flags);
      if (statusCode == noErr) {
          NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
       }
    }
    

    (4). 編碼回調,對視屏做真正的編碼處理

    1. 編碼成功后會回調之前輸入的函數didCompressH264.
    • 1> 先判斷是否是關鍵幀:

    如果是關鍵幀,則需要在寫入關鍵幀之前,先寫入PPS、SPS的NALU
    取出PPS、SPS數據,并且封裝成NALU單元寫入文件

    • 2> 將I幀、P幀、B幀分別封裝成NALU單元寫入文件
    1. 寫入后,數據存儲方式:


      image8.png
    // 編碼完成回調
    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判斷狀態是否等于沒有錯誤
    if (status != noErr) {
        return;
    }
    
    // 2.根據傳入的參數獲取對象
    VideoEncode* encoder = (__bridge VideoEncode*)outputCallbackRefCon;
    
    // 3.判斷是否是關鍵幀
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    // 判斷當前幀是否為關鍵幀
    // 獲取sps & pps數據
    if (isKeyframe)
    {
        // 獲取編碼后的信息(存儲于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 獲取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        
        // 獲取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
        
        // 裝sps/pps轉成NSData,以方便寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 寫入文件
        [encoder gotSpsPps:sps pps: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; // 返回的nalu數據前四個字節不是0001的startcode,而是大端模式的幀長度length
        
        // 循環獲取nalu數據
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 從大端轉系統端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            
            // 移動到寫一個塊,轉成NALU單元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
      }
    }
    

    (5). 碼流存儲

     // sps和pps存儲
     - (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps{
            // 1.拼接NALU的header
            const char bytes[] = "\x00\x00\x00\x01";
            size_t length = (sizeof bytes) - 1;
            NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
            // 2.將NALU的頭&NALU的體寫入文件
            [self.fileHandle writeData:ByteHeader];
            [self.fileHandle writeData:sps];
            [self.fileHandle writeData:ByteHeader];
            [self.fileHandle writeData:pps];    
    }
    
     //  其他信息存儲
     - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame{
             NSLog(@"gotEncodedData %d", (int)[data length]);
             if (self.fileHandle != NULL){
                 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];
                 [self.fileHandle writeData:ByteHeader];
                 [self.fileHandle writeData:data];
             }
     }
    
  • 結束編碼

    - (void)endEncode{
      VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
       VTCompressionSessionInvalidate(self.compressionSession);
       CFRelease(self.compressionSession);
       self.compressionSession = NULL;
    }
    

軟編碼

安裝 FFmpeg

  • 安裝
    • $ ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
    • $ brew install ffmpeg
  • 使用
    • 轉化格式: $ ffmpeg -i xxx.webm xxx.mp4
    • 分離視頻: $ ffmpeg -i xxx.mp4 -vcodec copy -an xxx.mp4
    • 分離音頻: $ ffmpeg -i xxx.mp4 -acodec copy -vn xxx.aac

編譯FFmpeg-iOS

  • 下載編譯FFmpeg所需要的腳本文件gas-preprocessor.pl

如果/usr/local沒有權限Operation not permitted
按如下操作:

1、關閉 Rootless

  • 重啟MAC電腦
  • 開機時(聽到開機啟動聲音),按下Command+RCommand+Option+R, 進入恢復模式
  • 在上面的菜單實用工具中找到并打開Terminal終端, 輸入如下命令$ csrutil disable
  • 重啟電腦.

2、給/usr/local增加讀寫權限

  • 進入終端輸入$ sudo chown -R $(whoami) /usr/local

3、開啟 Rootless

  • 為了系統安全,重新開啟Rootless
  • 重復步驟1,開啟Rootless: $ csrutil enable
  • 下載腳本FFmpeg-iOS腳本

    編譯過程中有可能需要安裝yasmnasm,如果需要,直接安裝即可
    $ brew install yasm 、 $ brew install nasm

編譯X264

集成FFmpeg

  • 將執行腳本./build-ffmpeg.sh./build-x264.sh生成的文件FFmpeg-iOSx264-iOS拖入到工程中。

  • 添加依賴庫:VideoToolbox.framework、CoreMedia.frameworkAVFoundation.frameworklibiconv.tbd 、libbz2.tbd 、libz.tbd

  • 采集視屏信息

- (void)startCapture:(UIView *)preview
{
    // 1.獲取沙盒路徑
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"video.h264"];
    
    // 2.開始編碼
    [self.encoder setFileSavedPath:file];
    // 特別注意: 寬度&高度
    [self.encoder setX264ResourceWithVideoWidth:480 height:640 bitrate:1500000];
    
    // 1.創建捕捉會話
    AVCaptureSession *session = [[AVCaptureSession alloc] init];
    session.sessionPreset = AVCaptureSessionPreset640x480;
    self.captureSession = session;
    
    // 2.設置輸入設備
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error = nil;
    AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
    [session addInput:input];
    
    
    // 3.添加預覽圖層
    AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    previewLayer.frame = preview.bounds;
    [preview.layer insertSublayer:previewLayer atIndex:0];
    self.previewLayer = previewLayer;
    
    // 4.添加輸出設備
    AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
    self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    [output setSampleBufferDelegate:self queue:self.captureQueue];
    NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
                              [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
                              kCVPixelBufferPixelFormatTypeKey,
                              nil];
    
    output.videoSettings = settings;
    output.alwaysDiscardsLateVideoFrames = YES;
    [session addOutput:output];
    
    // 5.設置錄制視頻的方向
    AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
    [connection setVideoOrientation:previewLayer.connection.videoOrientation];
    
    // 6.開始捕捉
    [self.captureSession startRunning];
}
  • 獲取到視屏幀
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    [self.encoder encoderToH264:sampleBuffer];
}
  • 對視屏幀進行軟編碼
    (1). 初始化X264

    - (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
    {
        // 1.默認從第0幀開始(記錄當前的幀數)
        framecnt = 0;
        
        // 2.記錄傳入的寬度&高度
        encoder_h264_frame_width = width;
        encoder_h264_frame_height = height;
        
        // 3.注冊FFmpeg所有編解碼器(無論編碼還是解碼都需要該步驟)
        av_register_all();
        
        // 4.初始化AVFormatContext: 用作之后寫入視頻幀并編碼成 h264,貫穿整個工程當中(釋放資源時需要銷毀)
        pFormatCtx = avformat_alloc_context();
        
        // 5.設置輸出文件的路徑
        fmt = av_guess_format(NULL, out_file, NULL);
        pFormatCtx->oformat = fmt;
        
        // 6.打開文件的緩沖區輸入輸出,flags 標識為  AVIO_FLAG_READ_WRITE ,可讀寫
        if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
            printf("Failed to open output file! \n");
            return -1;
        }
        
        // 7.創建新的輸出流, 用于寫入文件
        video_st = avformat_new_stream(pFormatCtx, 0);
        
        // 8.設置 20 幀每秒 ,也就是 fps 為 20
        video_st->time_base.num = 1;
        video_st->time_base.den = 25;
        
        if (video_st==NULL){
            return -1;
        }
        
        // 9.pCodecCtx 用戶存儲編碼所需的參數格式等等
        // 9.1.從媒體流中獲取到編碼結構體,他們是一一對應的關系,一個 AVStream 對應一個  AVCodecContext
        pCodecCtx = video_st->codec;
        
        // 9.2.設置編碼器的編碼格式(是一個id),每一個編碼器都對應著自己的 id,例如 h264 的編碼 id 就是 AV_CODEC_ID_H264
        pCodecCtx->codec_id = fmt->video_codec;
        
        // 9.3.設置編碼類型為 視頻編碼
        pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
        
        // 9.4.設置像素格式為 yuv 格式
        pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
        
        // 9.5.設置視頻的寬高
        pCodecCtx->width = encoder_h264_frame_width;
        pCodecCtx->height = encoder_h264_frame_height;
        
        // 9.6.設置幀率
        pCodecCtx->time_base.num = 1;
        pCodecCtx->time_base.den = 15;
        
        // 9.7.設置碼率(比特率)
        pCodecCtx->bit_rate = bitrate;
        
        // 9.8.視頻質量度量標準(常見qmin=10, qmax=51)
        pCodecCtx->qmin = 10;
        pCodecCtx->qmax = 51;
        
        // 9.9.設置圖像組層的大小(GOP-->兩個I幀之間的間隔)
        pCodecCtx->gop_size = 250;
        
        // 9.10.設置 B 幀最大的數量,B幀為視頻圖片空間的前后預測幀, B 幀相對于 I、P 幀來說,壓縮率比較大,也就是說相同碼率的情況下,
        // 越多 B 幀的視頻,越清晰,現在很多打視頻網站的高清視頻,就是采用多編碼 B 幀去提高清晰度,
        // 但同時對于編解碼的復雜度比較高,比較消耗性能與時間
        pCodecCtx->max_b_frames = 5;
        
        // 10.可選設置
        AVDictionary *param = 0;
        // H.264
        if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
            // 通過--preset的參數調節編碼速度和質量的平衡。
            av_dict_set(&param, "preset", "slow", 0);
            
            // 通過--tune的參數值指定片子的類型,是和視覺優化的參數,或有特別的情況。
            // zerolatency: 零延遲,用在需要非常低的延遲的情況下,比如視頻直播的編碼
            av_dict_set(&param, "tune", "zerolatency", 0);
        }
        
        // 11.輸出打印信息,內部是通過printf函數輸出(不需要輸出可以注釋掉該局)
        av_dump_format(pFormatCtx, 0, out_file, 1);
        
        // 12.通過 codec_id 找到對應的編碼器
        pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
        if (!pCodec) {
            printf("Can not find encoder! \n");
            return -1;
        }
        
        // 13.打開編碼器,并設置參數 param
        if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
            printf("Failed to open encoder! \n");
            return -1;
        }
        
        // 13.初始化原始數據對象: AVFrame
        pFrame = av_frame_alloc();
        
        // 14.通過像素格式(這里為 YUV)獲取圖片的真實大小,例如將 480 * 720 轉換成 int 類型
        avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
        
        // 15.h264 封裝格式的文件頭部,基本上每種編碼都有著自己的格式的頭部,想看具體實現的同學可以看看 h264 的具體實現
        avformat_write_header(pFormatCtx, NULL);
        
        // 16.創建編碼后的數據 AVPacket 結構體來存儲 AVFrame 編碼后生成的數據
        av_new_packet(&pkt, picture_size);
        
        // 17.設置 yuv 數據中 y 圖的寬高
        y_size = pCodecCtx->width * pCodecCtx->height;
        
        return 0;
    }   
    

    (2). 對視屏幀進行編碼

    - (void)encoderToH264:(CMSampleBufferRef)sampleBuffer{
    // 1.通過CMSampleBufferRef對象獲取CVPixelBufferRef對象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.鎖定imageBuffer內存地址開始進行編碼
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.從CVPixelBufferRef讀取YUV的值
        // NV12和NV21屬于YUV格式,是一種two-plane模式,即Y和UV分為兩個Plane,但是UV(CbCr)為交錯存儲,而不是分為三個plane
        // 3.1.獲取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.獲取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根據像素獲取圖片的真實寬度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 獲取Y分量長度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);
        
        // 3.4.將NV12數據轉成YUV420數據
        UInt8 *pY = bufferPtr ;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width*height;
        UInt8 *pV = pU + width*height/4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV+=bytesrow1;
        }
        
        // 3.5.分別讀取YUV的數據
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf+ y_size;      // U
        pFrame->data[2] = picture_buf+ y_size*5/4;  // V
        
        // 4.設置當前幀
        pFrame->pts = framecnt;
        int got_picture = 0;
        
        // 4.設置寬度高度以及YUV各式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = AV_PIX_FMT_YUV420P;
        
        // 5.對編碼前的原始數據(AVFormat)利用編碼器進行編碼,將 pFrame 編碼后的數據傳入pkt 中
        int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
        if(ret < 0) {
            printf("Failed to encode! \n");
            
        }
        
        // 6.編碼成功后寫入 AVPacket 到 輸入輸出數據操作著 pFormatCtx 中,當然,記得釋放內存
        if (got_picture==1) {
            framecnt++;
            pkt.stream_index = video_st->index;
            ret = av_write_frame(pFormatCtx, &pkt);
            av_free_packet(&pkt);
        }
        
        // 7.釋放yuv數據
        free(yuv420_data);
      } 
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    }
    

    (3). 結束采集后,對資源釋放

    - (void)freeX264Resource{
    // 1.釋放AVFormatContext
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed\n");
    }
    
    // 2.將還未輸出的AVPacket輸出出來
    av_write_trailer(pFormatCtx);
    
    // 3.關閉資源
    if (video_st){
        avcodec_close(video_st->codec);
        av_free(pFrame);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
    }
    
    int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index){
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;
    
    while (1) {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
      }
    return ret;
    }
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容