我是靠谱客的博主 坚强溪流,最近开发中收集的这篇文章主要介绍AWLive 源码解读(视频编解码)视频编解码,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

视频编解码

视频编码

  1. 根据视频的宽高和编码格式创建编码器并设置编码器的一些属性
  2. 根据收到的CVPixelBufferRef 开始编码
  3. 在编码后的数据中分离出 sps&pps&nalu,
    1. 获取 sps&pps (Annex B或者 AVCC)
      • Annex B 格式 : 0x00000001(4字节) + sps + 0x00000001(4字节) + pps + 0x00000001(4字节) + NALU 数据 + 0x00000001(4字节) + NALU 数据 + … + 0x00000001(4字节) + NALU 数据
      • AVCC 格式 : dict[@“SampleDescriptionExtensionAtoms”][@“avcC”] + NALULen0(4字节) + NALU数据(NALULen0字节) + NALULen1(4字节) + NALU数据(NALULen1字节) + … + NALULenx(4字节) + NALU数据(NALULenx字节)
    2. 获取nalu
  4. 然后推流或者保存本地
  5. 完成后关闭推流器

[关键代码如下] :

// - 创建编码器
-(void)open{
/* 配置编码器, 这里并不是立刻开始编码, 只是配置编码器
1. 分配器
传 NULL
2. 分辨率的 width
3. 分辨率的 height
4. 编码类型 kCMVideoCodecType_H264
5. 编码规范 NULL
6. 源像素缓冲区 NULL
7. 压缩数据分配器 NULL
8. 编码完成的回调
9. self, 将 self 桥接过去
10. VTCompressionSessionRef
&_vEnSession
*/
OSStatus status = VTCompressionSessionCreate(NULL, (int32_t)(self.videoConfig.pushStreamWidth), (int32_t)self.videoConfig.pushStreamHeight, kCMVideoCodecType_H264, NULL, NULL, NULL, vtCompressionSessionCallback, (__bridge void * _Nullable)(self), &_vEnSession);
/** 编码器创建成功 配置编码器的参数 */
if (status == noErr) {
// 设置参数
// ProfileLevel,h264的协议等级,不同的清晰度使用不同的ProfileLevel。
kVTProfileLevel_H264_Baseline_AutoLevel 表示舍弃 B 帧
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
// 设置码率 通常设置为 宽 * 高 * 3 * 4 * 8;
//
int bitrate = width * height * 8 * 3 * 4;
//
CFNumberRef bitrateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bitrateRef);
//
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AverageBitRate, bitrate);
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(self.videoConfig.bitrate));
// 设置实时编码
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// 关闭重排Frame,因为有了B帧(双向预测帧,根据前后的图像计算出本帧)后,编码顺序可能跟显示顺序不同。此参数可以关闭B帧。
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// 关键帧最大间隔(GOP) 这里是 2s。
VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(self.videoConfig.fps * 2));
// 关于B帧 P帧 和I帧,请参考:http://blog.csdn.net/abcjennifer/article/details/6577934
//编码器参数设置完毕,准备开始,随时来数据,随时编码
status = VTCompressionSessionPrepareToEncodeFrames(_vEnSession);
if (status != noErr) {
[self onErrorWithCode:AWEncoderErrorCodeVTSessionPrepareFailed des:@"硬编码vtsession prepare失败"];
}
}else{
[self onErrorWithCode:AWEncoderErrorCodeVTSessionCreateFailed des:@"硬编码vtsession创建失败"];
}
}
// - 编码数据
-(void)encodeSample(CMSampleBufferRef)sampleBuffer{
if (!_vEnSession) {
return NULL;
}
uint32_t ptsMs = self.manager.timestamp + 1; //self.vFrameCount++ * 1000.f / self.videoConfig.fps;
CMTime pts = CMTimeMake(ptsMs, 1000);
CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(videoSample);
VTEncodeInfoFlags flags;
/*
1. VTCompressionSessionRef
2. 未编码的数据
3. 时间戳
4. 帧显示时间, 如果没有时间信息, KCMTimeInvalid
5. 帧属性, NULL
6. 编码过程的回调
7. flags 同步/异步
*/
//硬编码主要其实就这一句。将携带NV12数据的PixelBuf送到硬编码器中,进行编码。
// - 注意 : typedef CVImageBufferRef CVPixelBufferRef; 两者是同一种数据类型
status = VTCompressionSessionEncodeFrame(_vEnSession, pixelBuffer, pts, kCMTimeInvalid, NULL, NULL, &flags);
if (status != noErr) {
[self onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error"];
}
}
}
// - 编码成功的回调
static void vtCompressionSessionCallback (void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer ){
if (status == noErr && CMSampleBufferDataIsReady(sampleBuffer)) {
AWHWH264Encoder *encoder = (__bridge AWHWH264Encoder *)(outputCallbackRefCon);
BOOL isKeyFrame = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
// - 生成sps 和 pps 数据
if (!encoder.spsPpsData && isKeyFrame) {
CMFormatDescriptionRef sampleBufFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
NSDictionary *dict = (__bridge NSDictionary *)CMFormatDescriptionGetExtensions(sampleBufFormat);
/* ** ---- AVCC ---- ** 格式 */
encoder.spsPpsData = dict[@"SampleDescriptionExtensionAtoms"][@"avcC"];
/* ** ---- Annex B ---- ** 格式
size_t spsSize, spsCount;
const uint8_t *spsContent;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(sampleBufFormat, 0, &spsContent, &spsSize, &spsCount, 0);
size_t ppsSize, ppsCount;
const uint8_t *ppsContent;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(sampleBufFormat, 1, &ppsContent, &ppsSize, &ppsCount, 0);
NSMutableData *spsPpsData = [NSMutableData data];
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
[spsPpsData appendBytes:startCode length:4];
[spsPpsData appendBytes:spsContent length:spsSize];
[spsPpsData appendBytes:startCode length:4];
[spsPpsData appendBytes:ppsContent length:ppsSize];
encoder.spsPpsData = spsPpsData;
*/
needSpsPps = YES;
}
// - 生成 NALU
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t blockDataLen;
uint8_t *blockData;
status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &blockDataLen, (char **)&blockData);
if (status == noErr) {
size_t currReadPos = 0;
//一般情况下都是只有1个 nalu,在最开始编码的时候有2个,取最后1个
// - 循环获取每个 nalu 每个 nalu 前边都有 4 字节的 nalu 流长度 naluData 即为一帧h264数据。
while (currReadPos < blockDataLen - 4) {
uint32_t naluLen = 0;
memcpy(&naluLen, blockData + currReadPos, 4);
naluLen = CFSwapInt32BigToHost(naluLen);
encoder.naluData = [NSData dataWithBytes:blockData + currReadPos + 4 length:naluLen];
currReadPos += 4 + naluLen;
encoder.isKeyFrame = isKeyFrame;
}
/* AVCC 格式*/
uint32_t naluLen = (uint32_t)_naluData.length;
uint32_t bigNaluLen = CFSwapInt32HostToBig(naluLen);
NSMutableData *mutableData = [NSMutableData dataWithBytes:&bigNaluLen length:4];
[mutableData appendData:_naluData];
/* ** ---- Annex B 格式 ---- **
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
NSMutableData *mutableData = [NSMutableData dataWithBytes:startCode length:4];
[mutableData appendData:_naluData];
}
}

视频解码

  1. 根据sps 和 pps 生成解码器描述, 创建解码参数, 根据回调方法和回调参数生成回调结构体, 根据 以上生成的数据,生成解码器, 设置解码器的一些参数(Annex-B格式和AVCC不同, 生成解码器描述的方法不同)
  2. 根据传来的数据, 创建 CMBlockBuffer, 根据 CMBlockBuffer 创建 CMSampleBuffer, 解码创建的 sampleBuffer
  3. 在解码成功的回调中, 返回解码的 CVPixeBuffer;

[关键代码如下] :

// - 创建解码器
- (BOOL)initDecoder {
if (_decodeSesion) return true;
const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
int naluHeaderLen = 4;
/**
根据sps pps设置解码参数, 这种适用于Annex-B格式, 当格式为AVCC时候, 使用 CMVideoFormatDescriptionCreate 创建解码器描述
param kCFAllocatorDefault 分配器
param 2 参数个数
param parameterSetPointers 参数集指针
param parameterSetSizes 参数集大小
param naluHeaderLen nalu nalu start code 的长度 4
param _decodeDesc 解码器描述
return 状态
*/
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_decodeDesc);
if (status != noErr) {
NSLog(@"Video hard DecodeSession create H264ParameterSets(sps, pps) failed status= %d", (int)status);
return false;
}
/*
解码参数:
* kCVPixelBufferPixelFormatTypeKey:摄像头的输出数据格式
kCVPixelBufferPixelFormatTypeKey,已测可用值为
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,即420v
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,即420f
kCVPixelFormatType_32BGRA,iOS在内部进行YUV至BGRA格式转换
YUV420一般用于标清视频,YUV422用于高清视频,这里的限制让人感到意外。但是,在相同条件下,YUV420计算耗时和传输压力比YUV422都小。
* kCVPixelBufferWidthKey/kCVPixelBufferHeightKey: 视频源的分辨率 width*height
* kCVPixelBufferOpenGLCompatibilityKey : 它允许在 OpenGL 的上下文中直接绘制解码后的图像,而不是从总线和 CPU 之间复制数据。这有时候被称为零拷贝通道,因为在绘制过程中没有解码的图像被拷贝.
*/
NSDictionary *destinationPixBufferAttrs =
@{
(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], //iOS上 nv12(uvuv排布) 而不是nv21(vuvu排布)
(id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:_config.width],
(id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:_config.height],
(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
};
//解码回调设置
/*
VTDecompressionOutputCallbackRecord 是一个简单的结构体,它带有一个指针 (decompressionOutputCallback),指向帧解压完成后的回调方法。你需要提供可以找到这个回调方法的实例 (decompressionOutputRefCon)。VTDecompressionOutputCallback 回调方法包括七个参数:
参数1: 回调的引用
参数2: 帧的引用
参数3: 一个状态标识 (包含未定义的代码)
参数4: 指示同步/异步解码,或者解码器是否打算丢帧的标识
参数5: 实际图像的缓冲
参数6: 出现的时间戳
参数7: 出现的持续时间
*/
VTDecompressionOutputCallbackRecord callbackRecord;
callbackRecord.decompressionOutputCallback = videoDecompressionOutputCallback;
callbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
//创建session
/*!
@function
VTDecompressionSessionCreate
@abstract
创建用于解压缩视频帧的会话。
@discussion
解压后的帧将通过调用OutputCallback发出
@param
allocator
内存的会话。通过使用默认的kCFAllocatorDefault的分配器。
@param
videoFormatDescription 描述源视频帧
@param
videoDecoderSpecification 指定必须使用的特定视频解码器.NULL
@param
destinationImageBufferAttributes 描述源像素缓冲区的要求 NULL
@param
outputCallback 使用已解压缩的帧调用的回调
@param
decompressionSessionOut 指向一个变量以接收新的解压会话
*/
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationPixBufferAttrs), &callbackRecord, &_decodeSesion);
//判断一下status
if (status != noErr) {
NSLog(@"Video hard DecodeSession create failed status= %d", (int)status);
return false;
}
//设置解码会话属性(实时编码)
status = VTSessionSetProperty(_decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
NSLog(@"Vidoe hard decodeSession set property RealTime status = %d", (int)status);
return true;
}
// - 每次获取到流数据, 开始解码
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize {
CVPixelBufferRef outputPixelBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags flag0 = 0;
//创建blockBuffer
/*!
参数1: structureAllocator kCFAllocatorDefault
参数2: memoryBlock
frame
参数3: frame size
参数4: blockAllocator: Pass NULL
参数5: customBlockSource Pass NULL
参数6: offsetToData
数据偏移
参数7: dataLength 数据长度
参数8: flags 功能和控制标志
参数9: newBBufOut blockBuffer地址,不能为空
*/
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);
if (status != kCMBlockBufferNoErr) {
NSLog(@"Video hard decode create blockBuffer error code=%d", (int)status);
return outputPixelBuffer;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
//创建sampleBuffer
/*
参数1: allocator 分配器,使用默认内存分配, kCFAllocatorDefault
参数2: blockBuffer.需要编码的数据blockBuffer.不能为NULL
参数3: formatDescription,视频输出格式
参数4: numSamples.CMSampleBuffer 个数.
参数5: numSampleTimingEntries 必须为0,1,numSamples
参数6: sampleTimingArray.
数组.为空
参数7: numSampleSizeEntries 默认为1
参数8: sampleSizeArray
参数9: sampleBuffer对象
*/
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != noErr || !sampleBuffer) {
NSLog(@"Video hard decode create sampleBuffer failed status=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
//解码
//向视频解码器提示使用低功耗模式是可以的
VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback;
//异步解码
VTDecodeInfoFlags
infoFlag = kVTDecodeInfo_Asynchronous;
//解码数据
这里如果是同步的可以在解码后就得到解码后的数据, 也可以在回调中返回解码的结果
/*
参数1: 解码session
参数2: 源数据 包含一个或多个视频帧的CMsampleBuffer
参数3: 解码标志
参数4: 解码后数据outputPixelBuffer
参数5: 同步/异步解码标识
*/
status = VTDecompressionSessionDecodeFrame(_decodeSesion, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag);
if (status == kVTInvalidSessionErr) {
NSLog(@"Video hard decode
InvalidSessionErr status =%d", (int)status);
} else if (status == kVTVideoDecoderBadDataErr) {
NSLog(@"Video hard decode
BadData status =%d", (int)status);
} else if (status != noErr) {
NSLog(@"Video hard decode failed status =%d", (int)status);
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
// - 回调函数
void videoDecompressionOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CM_NULLABLE CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration ) {
if (status != noErr) {
NSLog(@"Video hard decode callback error status=%d", (int)status);
return;
}
//获取self
CCVideoDecoder *decoder = (__bridge CCVideoDecoder *)(decompressionOutputRefCon);
//调用回调队列
dispatch_async(decoder.callbackQueue, ^{
//将解码后的数据给decoder代理.viewController
[decoder.delegate videoDecodeCallback:imageBuffer];
//释放数据
CVPixelBufferRelease(imageBuffer);
});
}

源码地址1
源码地址2

最后

以上就是坚强溪流为你收集整理的AWLive 源码解读(视频编解码)视频编解码的全部内容,希望文章能够帮你解决AWLive 源码解读(视频编解码)视频编解码所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(46)

评论列表共有 0 条评论

立即
投稿
返回
顶部