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()
}
}
總體程式碼執行過程是
1、ViewController建立了一個懶載入的videoCapture,開始捕捉視訊時用videoCapture.startCapture(),結束(停止)時呼叫videoCapture.endCapture(),需要切換前置或後置攝像頭時呼叫videoCapture.switchFrontOrBack()
2、videoCapture初始化時把1的view傳進來,用於預覽層,初始化同時設定了setupVideo
3、startCapture開始時,作者介紹了兩種方式來使用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);
}
4、captureOutput是session.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 --- ");
}
}