一、前言
前一段時間一直在做新的項目,已經很久沒更新文章了,今天終于閑下來了,筆者很高興吶,趁這時間,分享一下(直播)拉流端視頻編碼。iOS8.0以后,可以直接用系統的VideoToolbox框架進行編解碼,CPU占用率相比較ffmpeg來說會低很多,在做拉流時,我就是用的這個框架。下面廢話也不多說了,直接講解步驟上代碼。
二、視頻硬編碼
1.編碼器類DDHardwareVideoEncoder.h中,該類繼承自DDVideoEncoding(編碼器抽象接口類)其中,DDLiveVideoConfiguration是視頻配置文件,里面是視頻幀率、輸入方向、分辨率等視頻相關屬性,具體文件實現如下:
#import "DDVideoEncoding.h"
@interface DDHardwareVideoEncoder : NSObject<DDVideoEncoding>
#pragma mark - Initializer
///=============================================================================
/// @name Initializer
///=============================================================================
- (nullable instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (nullable instancetype)new UNAVAILABLE_ATTRIBUTE;
@property (nonatomic, strong, nonnull) DDLiveVideoConfiguration *configuration;
// 是否插入關鍵幀
@property (assign, nonatomic) BOOL isInsertKeyFrame;
@end
2.編碼器抽象接口類DDVideoEncoding.h文件實現如下:
其中DDVideoFrame類是編碼成功后數據處理類,里面有每幀編碼成功后的data(h264裸流)、videoFrameRate(幀率)、frameCount(幀數)、timestamp(時間戳)等屬性。
#import <Foundation/Foundation.h>
#import "DDVideoFrame.h"
#import "DDLiveVideoConfiguration.h"
@protocol DDVideoEncoding;
/// 編碼器編碼后回調
@protocol DDVideoEncodingDelegate <NSObject>
@required
- (void)videoEncoder:(nullable id<DDVideoEncoding>)encoder videoFrame:(nullable DDVideoFrame*)frame;
@end
/// 編碼器抽象的接口
@protocol DDVideoEncoding <NSObject>
@required
- (void)encodeVideoData:(nullable CVImageBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp;
- (void)stopEncoder;
@optional
@property (nonatomic, assign) NSInteger videoBitRate;
- (nullable instancetype)initWithVideoStreamConfiguration:(nullable DDLiveVideoConfiguration*)configuration;
- (void)setDelegate:(nullable id<DDVideoEncodingDelegate>)delegate;
3.下面是具體實現的DDHardwareVideoEncoder.m類文件
#import "DDHardwareVideoEncoder.h"
#import <VideoToolbox/VideoToolbox.h>
@interface DDHardwareVideoEncoder (){
VTCompressionSessionRef compressionSession;
NSInteger frameCount;
NSData *sps;
NSData *pps;
}
@property (nonatomic,weak) id<DDVideoEncodingDelegate> h264Delegate;
@property (nonatomic) BOOL isBackGround;
@property (nonatomic) NSInteger currentVideoBitRate;
@property (assign, nonatomic) uint64_t lastTimestamp;
@end
@implementation DDHardwareVideoEncoder
#pragma mark -- LifeCycle
- (instancetype)initWithVideoStreamConfiguration:(DDLiveVideoConfiguration *)configuration{
if(self = [super init]){
_configuration = configuration;
[self initCompressionSession];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterBackground:) name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
return self;
}
- (void)updateConfiguration {
[self initCompressionSession];
self->sps = NULL;
}
- (void)clearCompressionSession {
if(compressionSession){
VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(compressionSession);
CFRelease(compressionSession);
compressionSession = NULL;
}
}
- (void)initCompressionSession{
[self clearCompressionSession];
[self configCompressionSession];
}
- (void)configCompressionSession {
// VideoCompressonOutputCallback回調函數:視頻圖像編碼成功后調用
OSStatus status = VTCompressionSessionCreate(NULL, _configuration.videoSize.width, _configuration.videoSize.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoCompressonOutputCallback, (__bridge void *)self, &compressionSession);
if(status != noErr){
return;
}
_currentVideoBitRate = _configuration.videoBitRate;
// 設置關鍵幀間隔
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval,(__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration,(__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
// 設置期望幀率
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_configuration.videoFrameRate));
//設置碼率,均值,單位是byte
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_configuration.videoBitRate)); // bps
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(_configuration.videoMaxBitRate), @1]); // Bps
// 設置實時編碼輸出(避免延遲)
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// status = VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CAVLC);
VTCompressionSessionPrepareToEncodeFrames(compressionSession);
}
- (void)setVideoBitRate:(NSInteger)videoBitRate{
if(_isBackGround) return;
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(_configuration.videoMaxBitRate), @1]);
_currentVideoBitRate = videoBitRate;
}
-(NSInteger)videoBitRate{
return _currentVideoBitRate;
}
- (void)dealloc{
if(compressionSession != NULL)
{
VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(compressionSession);
CFRelease(compressionSession);
compressionSession = NULL;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark -- DDVideoEncoder
- (void)encodeVideoData:(CVImageBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
if(_isBackGround) return;
NSInteger timeCha = timeStamp - self.lastTimestamp;
if (timeCha - 1000/(int32_t)_configuration.videoFrameRate < 0) {
return;
}
self.lastTimestamp = timeStamp;
frameCount ++;
CMTime presentationTimeStamp = CMTimeMake(frameCount, 1000);
VTEncodeInfoFlags flags;
CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
NSDictionary *properties = nil;
if(frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0 || self.isInsertKeyFrame == YES){
properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
}
NSNumber *timeNumber = @(timeStamp);
// 編碼,編碼成功后調用回調函數
OSStatus statusCode = VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
if (frameCount > 262143) { // 該數值根據后臺要求定義
frameCount = 0;
}
self.isInsertKeyFrame = NO;
if (statusCode != noErr) {
VTCompressionSessionInvalidate(compressionSession);
compressionSession = NULL;
return;
}
}
- (void)stopEncoder{
VTCompressionSessionCompleteFrames(compressionSession, kCMTimeIndefinite);
}
- (void)setDelegate:(id<DDVideoEncodingDelegate>)delegate{
_h264Delegate = delegate;
}
#pragma mark -- NSNotification
- (void)willEnterBackground:(NSNotification*)notification{
_isBackGround = YES;
}
- (void)willEnterForeground:(NSNotification*)notification{
[self initCompressionSession];
_isBackGround = NO;
}
#pragma mark -- VideoCallBack
// 視頻編碼成功后回調,將編碼成功的CMSampleBuffer轉換成H264碼流
static void VideoCompressonOutputCallback(void *VTref, void *VTFrameRef, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
if(!sampleBuffer) return;
CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
if(!array) return;
CFDictionaryRef dic = (CFDictionaryRef)CFArrayGetValueAtIndex(array, 0);
if(!dic) return;
BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
uint64_t timeStamp = [((__bridge_transfer NSNumber*)VTFrameRef) longLongValue];
DDHardwareVideoEncoder *videoEncoder = (__bridge DDHardwareVideoEncoder *)VTref;
if(status != noErr){
return;
}
if (keyframe && !videoEncoder->sps)
{
// 描述信息
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t sparameterSetSize, sparameterSetCount;
const uint8_t *sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
if (statusCode == noErr)
{
size_t pparameterSetSize, pparameterSetCount;
const uint8_t *pparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
if (statusCode == noErr)
{
videoEncoder->sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
videoEncoder->pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
}
}
}
// 編碼后的數據結構
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length, totalLength;
char *dataPointer;
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
if (statusCodeRet == noErr) {
size_t bufferOffset = 0;
static const int AVCCHeaderLength = 4;
while (bufferOffset < totalLength - AVCCHeaderLength) {
// Read the NAL unit length
uint32_t NALUnitLength = 0;
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
DDVideoFrame *videoFrame = [[DDVideoFrame alloc] init];
videoFrame.timestamp = timeStamp;
videoFrame.isKeyFrame = keyframe;
videoFrame.frameCount = videoEncoder->frameCount;
videoFrame.videoFrameRate = videoEncoder.configuration.videoFrameRate;
videoFrame.videoWidth = videoEncoder.configuration.videoSize.width;
videoFrame.videoHeight = videoEncoder.configuration.videoSize.height;
NSData *h264Data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
// 以后數據處理,根據各自后臺要求數據格式處理
NSMutableData *mData = [NSMutableData data];
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
if (keyframe) {
[mData appendData:ByteHeader];
[mData appendData:videoEncoder->sps];
[mData appendData:ByteHeader];
[mData appendData:videoEncoder->pps];
}
[mData appendData:ByteHeader];
[mData appendData:h264Data];
videoFrame.data = mData;
if(videoEncoder.h264Delegate && [videoEncoder.h264Delegate respondsToSelector:@selector(videoEncoder:videoFrame:)]){
[videoEncoder.h264Delegate videoEncoder:videoEncoder videoFrame:videoFrame]; // 數據傳出去之后,實現該代理方法,根據后臺數據格式進行數據封裝,然后發送
}
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
@end