h264視頻編碼

(由於本文使用swift3調用底層C來實現 h264硬編碼,所以讀者需要對swift3 OC  C均要有一定的基礎才能看懂本文,文後附有代碼執行思路)

創建一個類用於設置h264的設置屬性(參數通過類的對象的屬性的方式來做)

//
//  TGVTSessionSetProperty.h
//  videocapture
//
//  Created by targetcloud on 2017/3/31.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface TGVTSessionSetProperty : NSObject
@property(nonatomic,assign) int width;
@property(nonatomic,assign) int height;
@property(nonatomic,assign) int expectedFrameRate;
@property(nonatomic,assign) int averageBitRate;
@property(nonatomic,assign) int maxKeyFrameInterval;
@end


//
//  TGVTSessionSetProperty.m
//  videocapture
//
//  Created by targetcloud on 2017/3/31.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

#import "TGVTSessionSetProperty.h"

@implementation TGVTSessionSetProperty

@end

每次創建編碼器模式

//
//  TGH264Encoder.h
//  videocapture
//
//  Created by targetcloud on 2017/3/30.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>
@class TGVTSessionSetProperty;

@interface TGH264Encoder : NSObject
- (instancetype)initWithProperty : (TGVTSessionSetProperty *) properties;
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end

//
//  TGH264Encoder.m
//  videocapture
//
//  Created by targetcloud on 2017/3/30.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

#import "TGH264Encoder.h"
#import "TGVTSessionSetProperty.h"
@interface TGH264Encoder()
@property (nonatomic, assign) NSInteger frameID;
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@property(nonatomic, strong) TGVTSessionSetProperty * properties ;
@end

@implementation TGH264Encoder

- (instancetype)initWithProperty : (TGVTSessionSetProperty *) properties {
    if (self = [super init]) {
        self.properties = properties;
        [self setupFileHandle];
        [self setupVideoSession];
    }
    return self;
}

- (void)setupFileHandle {
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]
                      stringByAppendingPathComponent:@"videoAudioCapture.h264"];
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}


- (void)setupVideoSession {
    self.frameID = 0;
    int width = self.properties.width;
    int height = self.properties.height;
    
    // 創建CompressionSession對象,該對象用於對畫面進行編碼,kCMVideoCodecType_H264 : 表示使用h.264進行編碼,h264VTCompressionOutputCallback : 當一次編碼結束會在該函數進行回調,可以在該函數中將數據,寫入文件中
    VTCompressionSessionCreate(NULL,
                               width,
                               height,
                               kCMVideoCodecType_H264,
                               NULL,
                               NULL,
                               NULL,
                               h264VTCompressionOutputCallback,
                               (__bridge void *)(self),
                               &_compressionSession);
    
    // 設置實時編碼輸出(直播是實時輸出,否則會有延遲)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nonnull)(@YES));//kCFBooleanTrue
    
    // 設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)
    int fps = self.properties.expectedFrameRate;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    // 設置比特率(或叫碼率: 編碼效率, 碼率越高則畫面越清晰)
    int bitRate = self.properties.averageBitRate;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);//bit
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);//byte
    
    // 設置關鍵幀(GOPsize)間隔
    int frameInterval = self.properties.maxKeyFrameInterval;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 設置結束, 準備進行編碼
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}

// 編碼完成回調
void h264VTCompressionOutputCallback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    if (status != noErr) {
        return;
    }
    TGH264Encoder* encoder = (__bridge TGH264Encoder*)outputCallbackRefCon;
    
    //判斷是否是關鍵幀
    //bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments,0);
    BOOL isKeyframe = !CFDictionaryContainsKey(dict,kCMSampleAttachmentKey_NotSync);
    
    if (isKeyframe){//是關鍵幀則獲取sps & pps數據
        // 獲取編碼後的信息
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 獲取SPS
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, NULL );
        
        // 獲取PPS
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, NULL );
        
        // 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 h264AVCCHeaderLength = 4;
        // 循環獲取NALU
        while (bufferOffset < totalLength - h264AVCCHeaderLength) {//一幀的圖像可能需要寓情於景入多個NALU單元,slice切片處理
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, h264AVCCHeaderLength);//NALU length
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);// 從h264編碼的數據的大端模式(字節序)轉系統端模式
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + h264AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            bufferOffset += h264AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps{
    // NALU header
    const char bytes[] = "\x00\x00\x00\x01";//有一個隱藏的'\0'結束符 所以要-1
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    [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];
    }
}

//從這裏開始 -> h264VTCompressionOutputCallback
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);//將sampleBuffer轉成imageBuffer
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, self.properties.expectedFrameRate);//PTS DTS 根據當前的幀數,創建CMTime的時間
    VTEncodeInfoFlags flag;
    
    // 開始編碼該幀數據
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL,
                                                          (__bridge void * _Nullable)(self),//h264VTCompressionOutputCallback sourceFrameRefCon
                                                          &flag);//h264VTCompressionOutputCallback infoFlags
    if (statusCode == noErr) {
        NSLog(@" --- H264: VTCompressionSessionEncodeFrame Success --- ");
    }
}

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

@end

使用

//
//  TGVideoCapture.swift
//  videocapture
//
//  Created by targetcloud on 2017/3/30.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

import UIKit
import AVFoundation

class TGVideoCapture: NSObject {
    fileprivate lazy var videoQueue = DispatchQueue.global()
    fileprivate lazy var audioQueue = DispatchQueue.global()
    fileprivate lazy var session : AVCaptureSession = {
        let session = AVCaptureSession()
        session.sessionPreset = AVCaptureSessionPreset1280x720;
        return session
    }()
    
    //MARK:- 每次創建方式 1
    fileprivate var encoder : TGH264Encoder?
    
    fileprivate lazy var previewLayer : AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
    fileprivate var connection : AVCaptureConnection?
    fileprivate var videoOutput : AVCaptureVideoDataOutput?
    fileprivate var videoInput : AVCaptureDeviceInput?
    fileprivate var view : UIView
    
    init(_ view : UIView){
        self.view = view
        super.init()
        setupVideo()
        setupAudio()
    }

    func startCapture() {
        //MARK:- 每次創建方式 1(每次開始都是一個新的encoder)
        encoder =  { () -> TGH264Encoder! in
            let p  = TGVTSessionSetProperty()
            p.height = 1280
            p.width = 720
            p.expectedFrameRate = 30
            p.averageBitRate = 1280*720//1920*1080 1280*720 720*576 640*480 480*360
            p.maxKeyFrameInterval = 30//GOP大小 數值越大,壓縮後越小
            return TGH264Encoder(property: p)
        }()
        
        
        if connection?.isVideoOrientationSupported ?? false {
            connection?.videoOrientation = .portrait
        }
        connection?.preferredVideoStabilizationMode = .auto
        
        previewLayer.frame = view.bounds
        view.layer.insertSublayer(previewLayer, at: 0)
        session.startRunning()
    }

    func endCapture() {
        session.stopRunning()
        previewLayer.removeFromSuperlayer()
        
        //MARK:- 每次創建方式 3
        encoder?.endEncode()
    }
    

    func switchFrontOrBack() {
        // CATransition
        let rotaionAnim = CATransition()
        rotaionAnim.type = "oglFlip"
        rotaionAnim.subtype = "fromLeft"
        rotaionAnim.duration = 0.5
        view.layer.add(rotaionAnim, forKey: nil)
        
        // Check Current videoInput
        guard let videoInput = videoInput else { return }
        
        // Change Position
        let position : AVCaptureDevicePosition = videoInput.device.position == .front ? .back : .front
        
        // New DeviceInput
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else { return }
        guard let newDevice = devices.filter({$0.position == position}).first else { return }
        guard let newVideoInput = try? AVCaptureDeviceInput(device: newDevice) else { return }
        
        // Remove videoInput & Add newVideoInput
        session.beginConfiguration()
        session.removeInput(videoInput)
        session.addInput(newVideoInput)
        session.commitConfiguration()
        
        // Save Current videoInput
        self.videoInput = newVideoInput
        
        // portrait
        connection = videoOutput?.connection(withMediaType: AVMediaTypeVideo)
        if connection?.isVideoOrientationSupported ?? false {
            connection?.videoOrientation = .portrait
        }
        connection?.preferredVideoStabilizationMode = .auto
    }
}

extension TGVideoCapture {
    fileprivate func setupVideo() {
        //info.plist add Privacy - Camera Usage Description
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else {return}
        guard let device = devices.filter({$0.position == .back}).first else {return}
        guard let videoInput = try? AVCaptureDeviceInput(device: device) else {return}
        if session.canAddInput(videoInput){
            session.addInput(videoInput)
        }

        self.videoInput = videoInput
        
        let videoOutput = AVCaptureVideoDataOutput()
        videoOutput.setSampleBufferDelegate(self, queue:videoQueue)
        videoOutput.alwaysDiscardsLateVideoFrames = true
        if session.canAddOutput(videoOutput){
            session.addOutput(videoOutput)
        }
        
        connection = videoOutput.connection(withMediaType: AVMediaTypeVideo)
        self.videoOutput = videoOutput
    }
    
    fileprivate func setupAudio() {
        //info.plist add Privacy - Microphone Usage Description
        guard let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio) else {return}
        guard let audioInput = try? AVCaptureDeviceInput(device: device) else {return}
        if session.canAddInput(audioInput){
            session.addInput(audioInput)
        }
        
        let audioOutput = AVCaptureAudioDataOutput()
        audioOutput.setSampleBufferDelegate(self, queue:audioQueue)
        if session.canAddOutput(audioOutput){
            session.addOutput(audioOutput)
        }
    }
}

extension TGVideoCapture : AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate{
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        if connection == self.connection {
            print("-採集到視頻畫面");
        }else{
            print("採集到音頻數據-");
        }
        
        //MARK:- 每次創建方式 2
        encoder?.encode(sampleBuffer)
    }
}


懶加載方式創建編碼器模式

//
//  TGH264Encoder.m
//  videocapture
//
//  Created by targetcloud on 2017/3/30.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

#import "TGH264Encoder.h"
#import "TGVTSessionSetProperty.h"
@interface TGH264Encoder()
@property (nonatomic, assign) NSInteger frameID;
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@property(nonatomic, strong) TGVTSessionSetProperty * properties ;
@end

@implementation TGH264Encoder

- (instancetype)initWithProperty : (TGVTSessionSetProperty *) properties {
    if (self = [super init]) {
        self.properties = properties;
        [self setupFileHandle];
        [self setupVideoSession];
    }
    return self;
}

- (void)setupFileHandle {
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]
                      stringByAppendingPathComponent:@"videoAudioCapture.h264"];
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}


- (void)setupVideoSession {
    self.frameID = 0;
    int width = self.properties.width;
    int height = self.properties.height;
    
    // 創建CompressionSession對象,該對象用於對畫面進行編碼,kCMVideoCodecType_H264 : 表示使用h.264進行編碼,h264VTCompressionOutputCallback : 當一次編碼結束會在該函數進行回調,可以在該函數中將數據,寫入文件中
    VTCompressionSessionCreate(NULL,
                               width,
                               height,
                               kCMVideoCodecType_H264,
                               NULL,
                               NULL,
                               NULL,
                               h264VTCompressionOutputCallback,
                               (__bridge void *)(self),
                               &_compressionSession);
    
    // 設置實時編碼輸出(直播是實時輸出,否則會有延遲)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nonnull)(@YES));//kCFBooleanTrue
    
    // 設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)
    int fps = self.properties.expectedFrameRate;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    // 設置比特率(或叫碼率: 編碼效率, 碼率越高則畫面越清晰)
    int bitRate = self.properties.averageBitRate;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);//bit
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);//byte
    
    // 設置關鍵幀(GOPsize)間隔
    int frameInterval = self.properties.maxKeyFrameInterval;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 設置結束, 準備進行編碼
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}

// 編碼完成回調
void h264VTCompressionOutputCallback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    if (status != noErr) {
        return;
    }
    TGH264Encoder* encoder = (__bridge TGH264Encoder*)outputCallbackRefCon;
    
    //判斷是否是關鍵幀
    //bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments,0);
    BOOL isKeyframe = !CFDictionaryContainsKey(dict,kCMSampleAttachmentKey_NotSync);
    
    if (isKeyframe){//是關鍵幀則獲取sps & pps數據
        // 獲取編碼後的信息
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 獲取SPS
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, NULL );
        
        // 獲取PPS
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, NULL );
        
        // 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 h264AVCCHeaderLength = 4;
        // 循環獲取NALU
        while (bufferOffset < totalLength - h264AVCCHeaderLength) {//一幀的圖像可能需要寓情於景入多個NALU單元,slice切片處理
            uint32_t NALUnitLength = 0;
            memcpy(&NALUnitLength, dataPointer + bufferOffset, h264AVCCHeaderLength);//NALU length
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);// 從h264編碼的數據的大端模式(字節序)轉系統端模式
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + h264AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            bufferOffset += h264AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps{
    // NALU header
    const char bytes[] = "\x00\x00\x00\x01";//有一個隱藏的'\0'結束符 所以要-1
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    [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];
    }
}

//從這裏開始 -> h264VTCompressionOutputCallback
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);//將sampleBuffer轉成imageBuffer
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, self.properties.expectedFrameRate);//PTS DTS 根據當前的幀數,創建CMTime的時間
    VTEncodeInfoFlags flag;
    
    // 開始編碼該幀數據
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL,
                                                          (__bridge void * _Nullable)(self),//h264VTCompressionOutputCallback sourceFrameRefCon
                                                          &flag);//h264VTCompressionOutputCallback infoFlags
    if (statusCode == noErr) {
        NSLog(@" --- H264: VTCompressionSessionEncodeFrame Success --- ");
    }
}

- (void)endEncode {
    VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
    
    //以下代碼是結束編碼後 把此次的編碼改名存放,並重置videoAudioCapture.h264爲初始化狀態,適用於懶加載編碼器
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSString * dateStr = [formatter stringFromDate:[NSDate date]];
    
    [[NSFileManager defaultManager] copyItemAtPath:[ path stringByAppendingPathComponent:@"videoAudioCapture.h264"]
                                            toPath:[ path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.h264",dateStr]] error:NULL];
    [self setupFileHandle];
    
    //因爲外面是懶加載創建TGH264Encoder,所以這裏不置空Session,如果外面不是懶加載創建的,則置空,取消下面的三行註釋
    //VTCompressionSessionInvalidate(self.compressionSession);
    //CFRelease(self.compressionSession);
    //self.compressionSession = NULL;
}

@end


使用

//
//  TGVideoCapture.swift
//  videocapture
//
//  Created by targetcloud on 2017/3/30.
//  Copyright © 2017年 targetcloud. All rights reserved.
//

import UIKit
import AVFoundation

class TGVideoCapture: NSObject {
    fileprivate lazy var videoQueue = DispatchQueue.global()
    fileprivate lazy var audioQueue = DispatchQueue.global()
    fileprivate lazy var session : AVCaptureSession = {
        let session = AVCaptureSession()
        session.sessionPreset = AVCaptureSessionPreset1280x720;
        return session
    }()
    //MARK:- 懶方式 1
    fileprivate lazy var encoder : TGH264Encoder = {
        let p  = TGVTSessionSetProperty()
        p.height = 1280
        p.width = 720
        p.expectedFrameRate = 30
        p.averageBitRate = 1280*720//1920*1080 1280*720 720*576 640*480 480*360
        p.maxKeyFrameInterval = 30//GOP大小 數值越大,壓縮後越小
        return TGH264Encoder(property: p)
    }()
    
    fileprivate lazy var previewLayer : AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
    fileprivate var connection : AVCaptureConnection?
    fileprivate var videoOutput : AVCaptureVideoDataOutput?
    fileprivate var videoInput : AVCaptureDeviceInput?
    fileprivate var view : UIView
    
    init(_ view : UIView){
        self.view = view
        super.init()
        setupVideo()
        setupAudio()
    }

    func startCapture() {
        if connection?.isVideoOrientationSupported ?? false {
            connection?.videoOrientation = .portrait
        }
        connection?.preferredVideoStabilizationMode = .auto
        
        previewLayer.frame = view.bounds
        view.layer.insertSublayer(previewLayer, at: 0)
        session.startRunning()
    }

    func endCapture() {
        session.stopRunning()
        previewLayer.removeFromSuperlayer()
        
        //MARK:- 懶方式 3
        encoder.endEncode()
    }
    
    func switchFrontOrBack() {
        // CATransition
        let rotaionAnim = CATransition()
        rotaionAnim.type = "oglFlip"
        rotaionAnim.subtype = "fromLeft"
        rotaionAnim.duration = 0.5
        view.layer.add(rotaionAnim, forKey: nil)
        
        // Check Current videoInput
        guard let videoInput = videoInput else { return }
        
        // Change Position
        let position : AVCaptureDevicePosition = videoInput.device.position == .front ? .back : .front
        
        // New DeviceInput
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else { return }
        guard let newDevice = devices.filter({$0.position == position}).first else { return }
        guard let newVideoInput = try? AVCaptureDeviceInput(device: newDevice) else { return }
        
        // Remove videoInput & Add newVideoInput
        session.beginConfiguration()
        session.removeInput(videoInput)
        session.addInput(newVideoInput)
        session.commitConfiguration()
        
        // Save Current videoInput
        self.videoInput = newVideoInput
        
        // portrait
        connection = videoOutput?.connection(withMediaType: AVMediaTypeVideo)
        if connection?.isVideoOrientationSupported ?? false {
            connection?.videoOrientation = .portrait
        }
        connection?.preferredVideoStabilizationMode = .auto
    }
}

extension TGVideoCapture {
    fileprivate func setupVideo() {
        //info.plist add Privacy - Camera Usage Description
        guard let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as? [AVCaptureDevice] else {return}
        guard let device = devices.filter({$0.position == .back}).first else {return}
        guard let videoInput = try? AVCaptureDeviceInput(device: device) else {return}
        if session.canAddInput(videoInput){
            session.addInput(videoInput)
        }

        self.videoInput = videoInput
        
        let videoOutput = AVCaptureVideoDataOutput()
        videoOutput.setSampleBufferDelegate(self, queue:videoQueue)
        videoOutput.alwaysDiscardsLateVideoFrames = true
        if session.canAddOutput(videoOutput){
            session.addOutput(videoOutput)
        }
        
        connection = videoOutput.connection(withMediaType: AVMediaTypeVideo)
        self.videoOutput = videoOutput
    }
    
    fileprivate func setupAudio() {
        //info.plist add Privacy - Microphone Usage Description
        guard let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio) else {return}
        guard let audioInput = try? AVCaptureDeviceInput(device: device) else {return}
        if session.canAddInput(audioInput){
            session.addInput(audioInput)
        }
        
        let audioOutput = AVCaptureAudioDataOutput()
        audioOutput.setSampleBufferDelegate(self, queue:audioQueue)
        if session.canAddOutput(audioOutput){
            session.addOutput(audioOutput)
        }
    }
}

extension TGVideoCapture : AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate{
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        if connection == self.connection {
            print("-採集到視頻畫面");
        }else{
            print("採集到音頻數據-");
        }
        //MARK:- 懶方式 2
        encoder.encode(sampleBuffer)
    }
}



由於編碼器採用OC編碼,外層使用用swift3編碼,所以還有一個橋接文件

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "TGH264Encoder.h"
#import "TGVTSessionSetProperty.h"


UI(VC/控制器)最外層調用的是swift3寫的TGVideoCapture

//
//  ViewController.swift
//  videocapture
//
//  Created by targetcloud on 2016/11/12.
//  Copyright © 2016年 targetcloud. All rights reserved.
//

import UIKit

class ViewController: UIViewController {
    fileprivate lazy var videoCapture : TGVideoCapture = TGVideoCapture(self.view)
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func startCapture(_ sender: Any) {
        videoCapture.startCapture()
    }

    @IBAction func endCapture(_ sender: Any) {
        videoCapture.endCapture()
    }
    
    
    @IBAction func switchFrontOrBack(_ sender: Any) {
        videoCapture.switchFrontOrBack()
    }
}
 

總體代碼執行過程是

1ViewController創建了一個懶加載的videoCapture,開始捕捉視頻時用videoCapture.startCapture(),結束(停止)時調用videoCapture.endCapture(),需要切換前置或後置攝像頭時調用videoCapture.switchFrontOrBack()
2videoCapture初始化時把1view傳進來,用於預覽層,初始化同時設置了setupVideo
3startCapture開始時,作者介紹了兩種方式來使用h264編碼,根據需要來進行選擇,如果不需要每次創建h264編碼器,那麼請使用懶加載方式
   3.1、懶加載時,我們對 h264解碼器進行了各種屬性設置,根據需要在這裏進行設置

   

    fileprivatelazyvar encoder :TGH264Encoder = {

        let p  =TGVTSessionSetProperty()

        p.height =1280

        p.width =720

        p.expectedFrameRate =30

        p.averageBitRate =1280*720//1920*1080 1280*720 720*576 640*480 480*360

        p.maxKeyFrameInterval =30//GOP大小數值越大,壓縮後越小

        returnTGH264Encoder(property: p)//關鍵代碼

    }()

   3.2、在懶加載內部,我們用TGVTSessionSetProperty正式對各種屬性進行了設置,主要對回調進行了設置h264VTCompressionOutputCallback

       - (instancetype)initWithProperty : (TGVTSessionSetProperty *) properties;


       - (instancetype)initWithProperty : (TGVTSessionSetProperty *) properties {

         if (self = [superinit]) {

            self.properties = properties;

            [selfsetupFileHandle];

            [selfsetupVideoSession];

        }

        returnself;

      }


      - (void)setupVideoSession {

    self.frameID =0;

    int width =self.properties.width;

    int height =self.properties.height;

    

    //創建CompressionSession對象,該對象用於對畫面進行編碼,kCMVideoCodecType_H264 : 表示使用h.264進行編碼,h264VTCompressionOutputCallback :當一次編碼結束會在該函數進行回調,可以在該函數中將數據,寫入文件中

    VTCompressionSessionCreate(NULL,

                               width,

                               height,

                               kCMVideoCodecType_H264,

                               NULL,

                               NULL,

                               NULL,

                               h264VTCompressionOutputCallback,

                               (__bridgevoid *)(self),

                               &_compressionSession);

    

   //設置實時編碼輸出(直播是實時輸出,否則會有延遲)

    VTSessionSetProperty(self.compressionSession,kVTCompressionPropertyKey_RealTime, (__bridgeCFTypeRef_Nonnull)(@YES));//kCFBooleanTrue

    

   //設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)

    int fps =self.properties.expectedFrameRate;

    CFNumberRef  fpsRef =CFNumberCreate(kCFAllocatorDefault,kCFNumberIntType, &fps);

    VTSessionSetProperty(self.compressionSession,kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

    

   //設置比特率(或叫碼率:編碼效率,碼率越高則畫面越清晰)

    int bitRate =self.properties.averageBitRate;

    CFNumberRef bitRateRef =CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type, &bitRate);

    VTSessionSetProperty(self.compressionSession,kVTCompressionPropertyKey_AverageBitRate, bitRateRef);//bit

    NSArray *limit =@[@(bitRate *1.5/8),@(1)];

    VTSessionSetProperty(self.compressionSession,kVTCompressionPropertyKey_DataRateLimits, (__bridgeCFArrayRef)limit);//byte

    

    //設置關鍵幀(GOPsize)間隔

    int frameInterval =self.properties.maxKeyFrameInterval;

    CFNumberRef  frameIntervalRef =CFNumberCreate(kCFAllocatorDefault,kCFNumberIntType, &frameInterval);

    VTSessionSetProperty(self.compressionSession,kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);

    

   //設置結束,準備進行編碼

    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);

}


4captureOutputsession.startRunning()時會調用的,此時正式進入-(void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;

     觸發代碼是

encoder.encode(sampleBuffer)


5、 encodeSampleBuffer 將調用我們第3步中的 h264VTCompressionOutputCallback,h264VTCompressionOutputCallback完成編碼

//從這裏開始 -> h264VTCompressionOutputCallback
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);//將sampleBuffer轉成imageBuffer
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, self.properties.expectedFrameRate);//PTS DTS 根據當前的幀數,創建CMTime的時間
    VTEncodeInfoFlags flag;
    
    // 開始編碼該幀數據
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL,
                                                          (__bridge void * _Nullable)(self),//h264VTCompressionOutputCallback sourceFrameRefCon
                                                          &flag);//h264VTCompressionOutputCallback infoFlags
    if (statusCode == noErr) {
        NSLog(@" --- H264: VTCompressionSessionEncodeFrame Success --- ");
    }
}



發佈了131 篇原創文章 · 獲贊 14 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章