(由於本文使用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 --- ");
}
}