编解码协议H264(视频)和AAC(音频)有软编解码和硬编解码。
网络传输都是用的大端序(高地址低字节),H264网络传输的startcode是数据的length,不是0x00000001。NALU 有两种格式:Annex B 和 AVCC。Annex B 格式startcode以 0x 00 00 01 或 0x 00 00 00 01 开头, AVCC 格式以 NALU 的长度开头。
AAC也有两种传输格式:ADTS和ADIF;
- ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
- ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。
软编码:
- 软编码:使用CPU进行编码。编码框架ffmpeg+x264。
https://www.jianshu.com/p/e631b041e96d
https://www.jianshu.com/p/3de01105d735
硬编码:
- 硬编码:不使用CPU进行编码,使用显卡(GPU)进行硬件加速,专用的DSP、FPGA、ASIC芯片等硬件进行编码。ios上硬编码框架Video ToolBox和AudioToolbox。
- Intel硬编码使用Intel处理器内部集成的显卡进行硬件加速,qsv加速方法便对应着Intel硬编码。Intel硬编码对H.264加速效果明显,且不需要安装额外库(仅使用相应的ffmpeg命令)。
- NVIDIA硬编码使用英伟达的显卡对视频编码进行加速。CUDA加速方法对应着NVIDIA硬编码。使用英伟达硬编码之前需要安装CUDA与英伟达的必要驱动。安装好两个环境后就可以使用NVIDIA的硬编码了。英伟达关于视频的编解码提供了两个相关的 SDK:NVENC(硬编码)和NVCUVID(硬解码),前者负责硬件编码,二后者负责硬件解码。CUDA支持Windows、Linux、MacOS三种主流操作系统。https://blog.csdn.net/qq_29350001/article/details/75144665(CUDA详解)
- FFmpeg中也支持了硬编码,集成了显示视频处理模块。在命令行中使用
ffmpeg -hwaccels
可以查看ffmpeg支持的硬件加速方法。 - FFMPEG 目前存在一个编码器
nvenc
是对于NVIDIA的NVENC
的封装,通过使用它可以和 FFMPEG 无缝的整合起来。不过 FFMPEG 只存在NVENC
的接口,不存在NVCUVID(解码器)
的封装。如果需要实现相关的解码器可能需要自己实现 FFMPEG 接口。FFMPEG实现了对于Intel QSV
的封装。 - DXVA是微软定制的视频加速规范、在Linux 平台上则是由NVIDIA提供的VDPAU和Intel提供的VAAPI加速规范。
-
在不同平台上可通过不同API使用Intel GPU的硬件加速能力。目前主要由两套API:VAAPI以及libmfx。VAAPI (视频加速API,Video Acceleration API)包含一套开源的库(LibVA) 以及API规范, 用于硬件加速下的视频编解码以及处理,只有Linux上的驱动提供支持。libmfx。Intel Media SDK中的API规范,支持视频编解码以及媒体处理。支持Windows以及Linux。除了Intel自己的API,在Windows系统上还有其他API可使用Intel GPU的硬件加速能力,这些API属于Windows标准,由Intel显卡驱动实现。DXVA2 / D3D11VA。标准Windows API,支持通过Intel显卡驱动进行视频编解码,FFmpeg有对应实现。Media Foundation。标准Windows API,支持通过Intel显卡驱动进行视频编解码,FFmpeg不支持该API。https://blog.jianchihu.net/intel-gpu-hw-video-codec-develop.html
目前的主流GPU加速平台:
INTEL、AMD、NVIDIA
目前主流的GPU平台开发框架:
-
CUDA:NVIDIA的封闭编程框架,通过框架可以调用GPU计算资源
-
AMD APP:AMD为自己的GPU提出的一套通用并行编程框架,标准开放,通过在CPU、GPU同时支持OpenCL框架,进行计算力融合。
-
OpenCL:开放计算语言,为异构平台编写程序的该框架,异构平台可包含CPU、GPU以及其他计算处理器,目标是使相同的运算能支持不同平台硬件加速。
-
Inel QuickSync:集成于Intel显卡中的专用视频编解码模块。
https://www.jianshu.com/p/8423724dffc1
https://blog.csdn.net/haowei0926/article/details/56012139
ios中的硬编码:https://developer.apple.com/documentation/videotoolbox
ios上硬编码框架Video ToolBox和AudioToolbox。Video ToolBox是一个底层框架,可以直接访问硬件编码器和解码器。 它提供视频压缩和解压缩服务,并在CoreVideo
像素缓冲区中存储的光栅raster
图像格式之间进行转换。 这些服务以会话对象(压缩,解压缩和像素传输)的形式提供,它们以Core Foundation(CF)
类型呈现。 不需要直接访问硬件编码器和解码器的应用程序App就不需要直接使用VideoToolbox
。iOS 8.0及以上苹果开放了VideoToolbox框架来实现H264硬编码(H264是一种编解码协议,有多种编解码器能编解码H264,这里是利用硬件进行编解码,FFmpeg中可以利用硬编解码和软编解码)。
CVPixelBufferRef
/CVImageBufferRef
:存放编码前和解码后的图像数据(未压缩的数据),这两个是相同的对象。CMTime
:时间戳相关,时间以64-bit/32-bit的形式出现CMBlockBufferRef
:编码后输出的数据(压缩后的数据)CMFormatDescriptionRef
/CMVideoFormatDescriptionRef
:图像存储方式,编解码器等格式描述。这两个是相同的对象。CMSampleBufferRef
:存放编解码前后的视频图像的容器数据,iOS中表示一帧音频/视频数据
CMSampleBuffer 可能是一个压缩的数据,也可能是一个未压缩的数据。取决于 CMSampleBuffer 里面是 CMBlockBuffer(压缩后) 还是 CVPixelBuffer(未压缩)。
硬编码的步骤:从相机或读取视频文件输出的CVPixelBuffer(也是以CMSampleBufferRef封装形式存在)—>Encoder—>CMSampleBufferRef(编码后得到的数据封装)—>重新组装NALUs。
1.通过VTCompressionSessionCreate创建编码器
VTCompressionSessionCreate(
CM_NULLABLE CFAllocatorRef allocator,
int32_t width,
int32_t height,
CMVideoCodecType codecType,
CM_NULLABLE CFDictionaryRef encoderSpecification,
CM_NULLABLE CFDictionaryRef sourceImageBufferAttributes,
CM_NULLABLE CFAllocatorRef compressedDataAllocator,
CM_NULLABLE VTCompressionOutputCallback outputCallback,
void * CM_NULLABLE outputCallbackRefCon,
CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut)
allocator
:内存分配器,填NULL为默认分配器width
、height
:视频帧像素的宽高,如果编码器不支持这个宽高的话可能会改变codecType
:编码类型,枚举encoderSpecification
:指定特定的编码器,填NULL的话由VideoToolBox自动选择sourceImageBufferAttributes
:源像素缓冲区的属性,如果这个参数有值的话,VideoToolBox会创建一个缓冲池,不需要缓冲池可以设置为NULLcompressedDataAllocator
:压缩后数据的内存分配器,填NULL使用默认分配器outputCallback
:视频编码后输出数据回调函数outputCallbackRefCon
:回调函数中的自定义指针,我们通常传self,因为我们需要在C函数中调用self的方法,而C函数无法直接调self,compressionSessionOut
:编码器句柄,传入编码器的指针
2.通过VTSessionSetProperty设置编码器属性,是否实时编码输出、是否产生B帧、设置关键帧、设置期望帧率、设置码率、最大码率值等等。
VTSessionSetProperty(
// 解码会话
CM_NONNULL VTSessionRef session,
// 属性 KEY
CM_NONNULL CFStringRef propertyKey,
// 设置的属性值
CM_NULLABLE CFTypeRef propertyValue )
kVTCompressionPropertyKey_AverageBitRate
:设置编码的平均码率,单位是bps,这不是一个硬性指标,设置的码率会上下浮动。VideoToolBox框架只支持ABR模式。H264有4种码率控制方法:
CBR
(Constant Bit Rate)是以恒定比特率方式进行编码,有Motion发生时,由于码率恒定,只能通过增大QP来减少码字大小,图像质量变差,当场景静止时,图像质量又变好,因此图像质量不稳定。这种算法优先考虑码率(带宽)。VBR
(Variable Bit Rate)动态比特率,其码率可以随着图像的复杂程度的不同而变化,因此其编码效率比较高,Motion发生时,马赛克很少。码率控制算法根据图像内容确定使用的比特率,图像内容比较简单则分配较少的码率(似乎码字更合适),图像内容复杂则分配较多的码字,这样既保证了质量,又兼顾带宽限制。这种算法优先考虑图像质量。
*CVBR
(Constrained VariableBit Rate),这样翻译成中文就比较难听了,它是VBR的一种改进方法。但是Constrained又体现在什么地方呢?这种算法对应的Maximum bitRate恒定或者Average BitRate恒定。这种方法的兼顾了以上两种方法的优点:在图像内容静止时,节省带宽,有Motion发生时,利用前期节省的带宽来尽可能的提高图像质量,达到同时兼顾带宽和图像质量的目的。ABR
(Average Bit Rate) 在一定的时间范围内达到设定的码率,但是局部码率峰值可以超过设定的码率,平均码率恒定。可以作为VBR和CBR的一种折中选择。
kVTCompressionPropertyKey_ProfileLevel
:设置H264编码的画质,H264有4种Profile:BP、EP、MP、HP
BP(Baseline Profile)
:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;主要应用:可视电话,会议电视,和无线通讯等实时视频通讯领域EP(Extended profile)
:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;MP(Main profile)
:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC 和CABAC 的支持;主要应用:数字广播电视和数字视频存储HP(High profile)
:高级画质。在main Profile 的基础上增加了8×8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;应用于广电和存储领域
Level就多了,这里不一一列举,可参考h264 profile & level,iPhone上常用的方案如下:
- 实时直播:
低清Baseline Level 1.3
标清Baseline Level 3
半高清Baseline Level 3.1
全高清Baseline Level 4.1- 存储媒体:
低清 Main Level 1.3
标清 Main Level 3
半高清 Main Level 3.1
全高清 Main Level 4.1- 高清存储:
半高清 High Level 3.1
全高清 High Level 4.1
kVTCompressionPropertyKey_RealTime
:设置是否实时编码输出kVTCompressionPropertyKey_AllowFrameReordering
:配置是否产生B帧,High profile 支持 B 帧kVTCompressionPropertyKey_MaxKeyFrameInterval
、kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration
:配置I帧间隔
3.调用VTCompressionSessionPrepareToEncodeFrames准备编码
VTCompressionSessionPrepareToEncodeFrames( CM_NONNULL VTCompressionSessionRef session )
session:编码器句柄,传入编码器的指针
4.输入采集到的视频数据CVImageBufferRef /CVPixelBufferRef
,调用VTCompressionSessionEncodeFrame进行编码
VTCompressionSessionEncodeFrame(
CM_NONNULL VTCompressionSessionRef session,
CM_NONNULL CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime duration, // may be kCMTimeInvalid
CM_NULLABLE CFDictionaryRef frameProperties,
void * CM_NULLABLE sourceFrameRefCon,
VTEncodeInfoFlags * CM_NULLABLE infoFlagsOut )
session
:创建编码器时的句柄imageBuffer
:YUV数据,iOS通过摄像头采集出来的视频流数据类型是CMSampleBufferRef
,我们要从里面拿到CVImageBufferRef
来进行编码。通过CMSampleBufferGetImageBuffer
方法可以从sampleBuffer中获得imageBuffer。presentationTimeStamp
:这一帧的时间戳,单位是毫秒duration
:这一帧的持续时间,如果没有持续时间,填kCMTimeInvalid
frameProperties
:指定这一帧的属性,这里我们可以用来指定产生I帧encodeParams
:自定义指针infoFlagsOut
:用于接收编码操作的信息,不需要就置为NULL
5.获取到编码后的数据并进行处理并组装NALU,添加起始码"\x00\x00\x00\x01",如果这一帧是个关键帧,需要添加sps pps**等。将硬编码成功的CMSampleBuffer转换成H264码流,解析出参数集SPS & PPS,加上开始码组装成 NALU。提取出视频数据,将长度码转换为开始码,组成NALU,将NALU写入到文件中。NALU 只要有两种格式:Annex B 和 AVCC。Annex B 格式以 0x 00 00 01 或 0x 00 00 00 01 开头, AVCC 格式以所在 NALU 的长度开头。
编码后的数据通过步骤一 VTCompressionSessionCreate
方法中参数的回调函数encodeOutputDataCallback返回。编码后的数据以及这一帧的基本信息都在CMSampleBufferRef
中。
void encodeOutputDataCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CM_NULLABLE CMSampleBufferRef sampleBuffer){ }。
6、调用VTCompressionSessionCompleteFrames停止编码器
VT_EXPORT OSStatus
VTCompressionSessionCompleteFrames(
CM_NONNULL VTCompressionSessionRef session,//编码器句柄
CMTime completeUntilPresentationTimeStamp//kCMTimeInvalid等
)
7、调用VTCompressionSessionInvalidate销毁编码器
VTCompressionSessionInvalidate(编码器句柄compressionSessionRef);
CFRelease(编码器句柄compressionSessionRef);
_compressionSessionRef = NULL;
代码示范:
#import <VideoToolbox/VideoToolbox.h>
@interface Nextvc ()
{
NSInteger frameID;
VTCompressionSessionRef cEncodeingSession;//编码器上下文
dispatch_queue_t cEncodeQueue;
}
//videoToolbox硬编码
-(void)videoToolboxHardEncode{
frameID = 0;
int width = 480,height = 640;
//创建编码session
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession);
NSLog(@"H264:VTCompressionSessionCreate:%d",(int)status);
if (status != 0) {
NSLog(@"H264:Unable to create a H264 session");
return ;
}
/**
设置编码器属性
*/
//设置实时编码输出(避免延迟)
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel,kVTProfileLevel_H264_Baseline_AutoLevel);
//是否产生B帧(因为B帧在解码时并不是必要的,是可以抛弃B帧的)
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
//设置关键帧(GOPsize)间隔,GOP太小的话图像会模糊
int frameInterval = 10;
CFNumberRef frameIntervalRaf = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRaf);
//设置期望帧率,不是实际帧率
int fps = 10;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
//码率的理解:码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看
//码率计算公式,参考印象笔记
//设置码率、上限、单位是bps
int bitRate = width * height * 3 * 4 * 8;
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
//设置码率,均值,单位是byte
int bigRateLimit = width * height * 3 * 4;
CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
//准备编码
VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
}
/**
输入待编码数据CMSampleBufferRef,开始编码
@param sampleBuffer 待编码数据,可以是从摄像头获取的数据,也可以是从视频文件中获取的数据
@param forceKeyFrame 是否强制I帧
@return 结果
*/
- (BOOL)videoEncodeInputData:(CMSampleBufferRef)sampleBuffer forceKeyFrame:(BOOL)forceKeyFrame
{
if (NULL == cEncodeingSession)
{
return NO;
}
if (nil == sampleBuffer)
{
return NO;
}
CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000); // CMTimeMake(分子,分母);分子/分母 = 时间(秒)
CVImageBufferRef pixelBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
NSDictionary *frameProperties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @(forceKeyFrame)};
OSStatus status = VTCompressionSessionEncodeFrame(cEncodeingSession, pixelBuffer, kCMTimeInvalid, kCMTimeInvalid, (__bridge CFDictionaryRef)frameProperties, NULL, NULL);//第三个参数可以换成presentationTimeStamp
if (noErr != status)
{
NSLog(@"VEVideoEncoder::VTCompressionSessionEncodeFrame failed! status:%d", (int)status);
return NO;
}
return YES;
}
//VideoToolBox硬编码回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
if (noErr != status || nil == sampleBuffer)
{
NSLog(@"VEVideoEncoder::encodeOutputCallback Error : %d!", (int)status);
return;
}
if (nil == outputCallbackRefCon)
{
return;
}
if (!CMSampleBufferDataIsReady(sampleBuffer))
{
return;
}
if (infoFlags & kVTEncodeInfo_FrameDropped)
{
NSLog(@"VEVideoEncoder::H264 encode dropped frame.");
return;
}
Nextvc *encoder = (__bridge Nextvc *)outputCallbackRefCon;
const char header[] = "\x00\x00\x00\x01";
size_t headerLen = (sizeof header) - 1; // 最后一位是\0结束符,要减掉
NSData *headerData = [NSData dataWithBytes:header length:headerLen];
// // 判断是否是关键帧
// bool isKeyFrame = !CFDictionaryContainsKey((CFDictionaryRef)CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), (const void *)kCMSampleAttachmentKey_NotSync);
//判断当前帧是否为关键帧
CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
bool isKeyFrame = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
if (isKeyFrame)
{
NSLog(@"VEVideoEncoder::编码了一个关键帧");
//图像的存储方式,编解码器等格式描述
CMFormatDescriptionRef formatDescriptionRef = CMSampleBufferGetFormatDescription(sampleBuffer);
/*关键帧需要加上SPS、PPS信息
获取sps & pps 数据 只获取1次,保存在h264文件开头的第一帧中
sps(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位
CMVideoFormatDescriptionGetH264ParameterSetAtIndex获取sps和pps信息,并转换为二进制写入文件或者进行上传
*/
size_t sParameterSetSize;//参数集合占的字节大小
size_t sParameterSetCount;//参数集合元素个数
const uint8_t *sParameterSet;//参数集合
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescriptionRef, 0, &sParameterSet, &sParameterSetSize, &sParameterSetCount, 0);//index为0的位置是sps;
size_t pParameterSetSize, pParameterSetCount;
const uint8_t *pParameterSet;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescriptionRef, 1, &pParameterSet, &pParameterSetSize, &pParameterSetCount, 0);//index为1的位置是pps;
if (noErr == spsStatus && noErr == ppsStatus)
{
//把sps和pps参数集合转换成二进制数据,组装成sps帧和pps帧;
NSData *sps = [NSData dataWithBytes:sParameterSet length:sParameterSetSize];
NSData *pps = [NSData dataWithBytes:pParameterSet length:pParameterSetSize];
NSMutableData *spsData = [NSMutableData data];
[spsData appendData:headerData];
[spsData appendData:sps];
NSMutableData *ppsData = [NSMutableData data];
[ppsData appendData:headerData];
[ppsData appendData:pps];
}
}
//获取编码后的h264流数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length;//单个NALU长度
size_t totalLength;//所有NALU总长度
char *dataPointer;//指针偏移
// 通过 首地址blockBuffer 、单个长度length、 总长度totalLength通过dataPointer指针偏移做遍历
status = CMBlockBufferGetDataPointer(blockBuffer, 0, &length, &totalLength, &dataPointer);
if (noErr != status)
{
NSLog(@"VEVideoEncoder::CMBlockBufferGetDataPointer Error : %d!", (int)status);
return;
}
size_t bufferOffset = 0;//Nalu的开始位置,每次增加加一个stratcode+nalu的长度
static const int avcHeaderLength = 4;//返回的nalu数据前4个字节不是0x00000001的startcode,而是大端模式的帧长度length,读取数据时有个大小端模式:网络传输一般都是大端模式
while (bufferOffset < totalLength - avcHeaderLength)
{
// 读取 一单元长度的nalu数据
uint32_t nalUnitLength = 0;
memcpy(&nalUnitLength, dataPointer + bufferOffset, avcHeaderLength);//目标地址,源地址,字节数,(从源地址拷贝n个字节到目标地址),这里其实是设置每个nalUnitLength的值,即各个Nalu的长度
// 大端转小端(系统端是小端序)
nalUnitLength = CFSwapInt32BigToHost(nalUnitLength);
//获取nalu数据
NSData *frameData = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + avcHeaderLength) length:nalUnitLength];
//Nalu头+NALU数据
NSMutableData *outputFrameData = [NSMutableData data];
[outputFrameData appendData:headerData];
[outputFrameData appendData:frameData];
//可以把outputFrameData写入文件,然后就得到了H264编码的文件。
//读取下一个nalu 一次回调可能包含多个nalu数据,
bufferOffset += avcHeaderLength + nalUnitLength;
}
}
******audioToolBox硬编码******
编码步骤:
1.配置编码参数、获取编码器描述description、获取编码器
2.设置缓冲列表AudioBufferList
3.开始编码,将数据写入编码器AudioConverterFillComplexBuffer,
4.在回调函数中,将数据写入缓冲区
5.编码完成后,获取缓冲区列表数据outAudioBUfferList,添加ADTS头
6.将数据写入文件
代码示例:
{ AudioConverterRef _audioConverter;//音频编码上下文
size_t _pcmBufferSize;
char *_pcmBuffer;
size_t _aacBufferSize;
char *_aacBuffer;
}
@property(nonatomic,strong)NSFileHandle *audioFileHandle;
@property(nonatomic,strong)dispatch_queue_t encoderQueue;)
//创建存储音频的文件,先移除以前的文件,再重新创建
- (NSFileHandle *)audioFileHandle {
if (!_audioFileHandle) {
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString * filePath = [documentPath stringByAppendingPathComponent:@"demo01.aac"];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
NSAssert(createFile, @"create audio path error");
_audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
}
return _audioFileHandle;
}
- (id)init {
if (self = [super init]) {
_encoderQueue = dispatch_queue_create("aac encode queue", DISPATCH_QUEUE_SERIAL);
_audioConverter = NULL;
_pcmBufferSize = 0;
_pcmBuffer = NULL;
_aacBufferSize = 1024;
_aacBuffer = malloc(_aacBufferSize * sizeof(uint8_t));
memset(_aacBuffer, 0, _aacBufferSize);
}
return self;
}
//停止编码
- (void)stopEncodeAudio {
[self.audioFileHandle closeFile];
self.audioFileHandle = NULL;
}
// 配置编码参数
- (void)setupEncoderFromSampleBuffer:(CMSampleBufferRef)sampleBuffer {
NSLog(@"开始配置编码参数。。。。");
/*
AudioStreamBasicDescription是输入输出流的结构体描述,
*/
// 获取原音频声音格式设置
AudioStreamBasicDescription inAudioStreamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer));
AudioStreamBasicDescription outAudioStreamBasicDescription = {0};
/*
设置输出格式参数
*/
// 采样率,音频流,在正常播放情况下的帧率。如果是压缩的格式,这个属性表示解压缩后的帧率。帧率不能为0。
outAudioStreamBasicDescription.mSampleRate = inAudioStreamBasicDescription.mSampleRate;
// 格式 kAudioFormatMPEG4AAC = 'aac' ,
outAudioStreamBasicDescription.mFormatID = kAudioFormatMPEG4AAC;
// 标签格式 无损编码 , 无损编码 ,0表示没有
outAudioStreamBasicDescription.mFormatFlags = kMPEG4Object_AAC_LC;
// 每一个packet的音频数据大小。如果的动态大小,设置为0。动态大小的格式,需要用AudioStreamPacketDescription 来确定每个packet的大小。
outAudioStreamBasicDescription.mBytesPerPacket = 0;
// 每个packet的帧数。如果是未压缩的音频数据,值是1。动态帧率格式,这个值是一个较大的固定数字,比如说AAC的1024。如果是动态大小帧数(比如Ogg格式)设置为0。
outAudioStreamBasicDescription.mFramesPerPacket = 1024;
// 每帧的大小。每一帧的起始点到下一帧的起始点。如果是压缩格式,设置为0 。
outAudioStreamBasicDescription.mBytesPerFrame = 0;
// 声道数:1 单声道 2 立体声
outAudioStreamBasicDescription.mChannelsPerFrame = 1;
// 每采样点占用位数
outAudioStreamBasicDescription.mBitsPerChannel = 0;
// 保留参数(对齐当时)8字节对齐,填0.
outAudioStreamBasicDescription.mReserved = 0;
// 获取编码器描述
AudioClassDescription * description = [self getAudioClassDescriptionWithType:kAudioFormatMPEG4AAC fromManufacturer:kAppleSoftwareAudioCodecManufacturer];
// 创建编码器
/*
inAudioStreamBasicDescription 传入源音频格式
outAudioStreamBasicDescription 目标音频格式
第三个参数:传入音频编码器的个数
description 传入音频编码器的描述
*/
OSStatus status = AudioConverterNewSpecific(&inAudioStreamBasicDescription, &outAudioStreamBasicDescription, 1, description, &_audioConverter);
if (status != 0) {
NSLog(@"创建编码器失败");
}
}
// 获取编码器描述
/*type 编码格式
manufacturer 软/硬编 kAppleHardwareAudioCodecManufacturer、kAppleSoftwareAudioCodecManufacturer
*/
- (AudioClassDescription *)getAudioClassDescriptionWithType:(UInt32)type
fromManufacturer:(UInt32)manufacturer
{
NSLog(@"开始获取编码器。。。。");
// 选择aac编码
/*AudioClassDescription结构体包含以下成员
OSType mType;
OSType mSubType;
OSType mManufacturer;
*/
static AudioClassDescription desc;
UInt32 encoderS = type;
OSStatus status;
UInt32 size;
/*获取所用有的编码器属性信息
kAudioFormatProperty_Encoders 编码ID
编码说明大小
编码类型
属性当前值的大小
*/
status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders, sizeof(encoderS), &encoderS, &size);
if (status) {
NSLog(@"编码aac错误");
return nil;
}
// 计算编码器的个数
unsigned int count = size / sizeof(AudioClassDescription);
// 定义编码器数组
AudioClassDescription description[count];
//分配编码器属性信息到数组
status = AudioFormatGetProperty(kAudioFormatProperty_Encoders, sizeof(encoderS), &encoderS, &size, description);
for (unsigned int i = 0; i < count; i++) {
if (type == description[i].mSubType && manufacturer == description[i].mManufacturer) {
// 拷贝编码器到desc
memcpy(&desc, &description[i], sizeof(desc));
NSLog(@"找到aac编码器");
return &desc;
}
}
return nil;
}
// 编码数据
- (void)encodeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CFRetain(sampleBuffer);
dispatch_sync(_encoderQueue, ^{
if (!_audioConverter) {
// 配置编码参数
[self setupEncoderFromSampleBuffer:sampleBuffer];
}
// 获取CMBlockBufferRef
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
// 获取_pcmBufferSize 和 _pcmBuffer
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &self->_pcmBufferSize, &self->_pcmBuffer);
if (status != kCMBlockBufferNoErr) {
NSLog(@"获取 pcmBuffer 数据错误");
return ;
}
// 清空
memset(self->_aacBuffer, 0, self->_aacBufferSize);
// 初始化缓冲列表
AudioBufferList outAudioBufferList = {0}; // 结构体
// 缓冲区个数
outAudioBufferList.mNumberBuffers = 1;
// 渠道个数
outAudioBufferList.mBuffers[0].mNumberChannels = 1;
// 缓存区大小
outAudioBufferList.mBuffers[0].mDataByteSize = (int)self->_aacBufferSize;
// 缓冲区内容
outAudioBufferList.mBuffers[0].mData = self->_aacBuffer;
// 编码
AudioStreamPacketDescription * outPD = NULL;
UInt32 inPutSize = 1;
/*
_audioConverter 音频编码上下文
inInputDataProc 自己实现的编码数据的callback引用
self 获取的数据
inPutSize 输出数据的长度
outAudioBUfferList 输出的缓冲区列表数据
outPD 输出数据的描述
*/
status = AudioConverterFillComplexBuffer(self->_audioConverter,
inInputDataProc,
(__bridge void*)self,
&inPutSize,
&outAudioBufferList,
outPD
);
// 编码后完成,AudioConverterFillComplexBuffer方法返回的是AAC原始码流,需要在AAC每帧添加ADTS头
NSData * data = nil;
if (status == noErr) {
// 获取缓冲区的原始数据acc数据
NSData * rawAAC = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
// 加头ADTS
NSData * adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
NSMutableData * fullData = [NSMutableData dataWithData:adtsHeader];
[fullData appendData:rawAAC];
data = fullData;
} else {
NSLog(@"数据错误");
return;
}
// 写入数据
[self.audioFileHandle writeData:data];
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
});
}
// audioToolBox回调函数,将数据写入缓冲区
OSStatus inInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData)
{
// 编码器
Nextvc *encoder = (__bridge Nextvc *) inUserData;
// 编码包的数据
UInt32 requestPackes = *ioNumberDataPackets;
// 将ioData填充到缓冲区
size_t cp = [encoder copyPCMSamplesIntoBuffer:ioData];
if (cp < requestPackes) {
//PCM 缓冲区还没满
*ioNumberDataPackets = 0; // 清空
return -1;
}
*ioNumberDataPackets = 1;
return noErr;
}
// pcm -> 缓冲区
- (size_t)copyPCMSamplesIntoBuffer:(AudioBufferList*)ioData {
// 获取pcm大小
size_t os = _pcmBufferSize;
if (!_pcmBufferSize) {
return 0;
}
ioData->mBuffers[0].mData = _pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (int)_pcmBufferSize;
// 清空
_pcmBuffer = NULL;
_pcmBufferSize = 0;
return os;
}
/**
* Add ADTS header at the beginning of each and every AAC packet.
* This is needed as MediaCodec encoder generates a packet of raw
* AAC data.
*
* Note the packetLen must count in the ADTS header itself.
注意:packetLen 必须在ADTS头身计算
**/
- (NSData*)adtsDataForPacketLength:(NSUInteger)packetLength {
int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
int profile = 2;
int freqIdx = 4;
int chanCfg = 1;
NSUInteger fullLength = adtsLength + packetLength;
packet[0] = (char)0xFF;
packet[1] = (char)0xF9;
packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));
packet[4] = (char)((fullLength&0x7FF) >> 3);
packet[5] = (char)(((fullLength&7)<<5) + 0x1F);
packet[6] = (char)0xFC;
NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];
return data;
}
FFmpeg中的硬编码:
FFmpeg中的硬编码有videotoolbox(苹果的ios和MACos)、mediacodec(安卓的)、qsv(Intel的LIBMFX api)、DXVA(是微软定制的视频加速规范,如DXVA2 / D3D11VA)、VDPAU(在Linux 平台上由NVIDIA指定的加速规范)、VAAPI(在Linux 平台上由Intel提供的加速规范)
Intel支持的硬编码:
windows:libmfx(Intel自己的api,FFmpeg中qsv技术对外接口就是LIBMF)、DXVA2 / D3D11VA(微软出的对Intel支持的api,FFmpeg中有封装)、Media Foundation(微软出的对Intel支持的api)
linux:VAAPI(Intel自己的api,FFmpeg中有封装)、libmfx(Intel自己的api,FFmpeg中qsv技术对外接口就是LIBMF)
NVIDIA支持的硬编码:
windows:CUDA(NVIDIA自己的api,FFmpeg中封装包含NVENC(硬编码)和NVCUVID(硬解码))
linux:CUDA(NVIDIA自己的api,FFmpeg中封装包含NVENC(硬编码)和NVCUVID(硬解码))、VDPAU(FFmpeg中有封装)
硬解码:
问题和优化方案:https://www.jianshu.com/p/57581485717b
硬编解码图示:https://www.cnblogs.com/edisongz/p/7062098.html
******videoToolBox硬解码*********
解码流程:
NALU + SPS,PPS—>CMBlockBuffer—>CMSampleBufferRef,再将CMSampleBufferRef包装的帧数据输入到 VTDecompressionSessionDecodeFrame,通过回调中CVImageBufferRef 直接上传OpenGL ES 显示。
序列参数集SPS(sequence Parameter Set):作用于一系列连续的编码图像
图像参数集PPS(Picture Parameter Set):作用于编码视频序列中一个或多个独立的图像;
硬解码流程:
1、解析H264数据
解码前的CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer。需要从H264的码流里面提取出以上的三个信息。最后组合成CMSampleBuffer,提供给硬解码接口来进行解码工作。
NALU单元包含视频图像数据和H264的参数信息。其中视频图像数据就是CMBlockBuffer,而H264的参数信息则可以组合成FormatDesc。
2、初始化解码器(VTDecompressionSessionCreate
)
3、将解析后的H264数据送入解码器(VTDecompressionSessionDecodeFrame
)
4、解码器回调输出解码后的数据(CVImageBufferRef
)
代码示例:
/** sps数据 */
@property (nonatomic, assign) uint8_t *sps;
/** sps数据长度 */
@property (nonatomic, assign) NSInteger spsSize;
/** pps数据 */
@property (nonatomic, assign) uint8_t *pps;
/** pps数据长度 */
@property (nonatomic, assign) NSInteger ppsSize;
/** 解码器句柄 */
@property (nonatomic, assign) VTDecompressionSessionRef deocderSession;
/** 视频解码信息句柄 */
@property (nonatomic, assign) CMVideoFormatDescriptionRef decoderFormatDescription;
/*
*读取本地视频文件
*/
/**
解码NALU数据
@param naluData NALU数据
*/
-(void)decodeNaluData:(NSData *)naluData
{
uint8_t *frame = (uint8_t *)naluData.bytes;
uint32_t frameSize = (uint32_t)naluData.length;
// frame的前4位是NALU数据的开始码,也就是00 00 00 01,第5个字节是表示数据类型,转为10进制后,7是sps,8是pps,5是IDR(I帧)信息
int nalu_type = (frame[4] & 0x1F);
/* 将NALU的开始码替换成NALU的长度信息
方法一:
*/
// uint32_t nalSize = (uint32_t)(frameSize - 4);
// uint8_t *pNalSize = (uint8_t*)(&nalSize);
// frame[0] = *(pNalSize + 3);
// frame[1] = *(pNalSize + 2);
// frame[2] = *(pNalSize + 1);
// frame[3] = *(pNalSize);
//方法二:
uint32_t nalSize = (uint32_t)(frameSize - 4);
uint32_t *pNalSize = (uint32_t *)frame;
*pNalSize = CFSwapInt32HostToBig(nalSize);
switch (nalu_type)
{
case 0x05: // I帧
NSLog(@"NALU type is IDR frame");
if([self initH264Decoder])
{
[self decode:frame withSize:frameSize];
}
break;
case 0x07: // SPS
NSLog(@"NALU type is SPS frame");
_spsSize = frameSize - 4;
_sps = malloc(_spsSize);
memcpy(_sps, &frame[4], _spsSize);
break;
case 0x08: // PPS
NSLog(@"NALU type is PPS frame");
_ppsSize = frameSize - 4;
_pps = malloc(_ppsSize);
memcpy(_pps, &frame[4], _ppsSize);
break;
default: // B帧或P帧
NSLog(@"NALU type is B/P frame");
if([self initH264Decoder])
{
[self decode:frame withSize:frameSize];
}
break;
}
}
/**
初始化解码器
@return 结果
*/
-(BOOL)initH264Decoder
{
if(_deocderSession)
{
return YES;
}
const uint8_t* const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
// 根据sps pps创建解码视频参数
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, 4, &_decoderFormatDescription);
if(status != noErr)
{
NSLog(@"H264Decoder::CMVideoFormatDescriptionCreateFromH264ParameterSets failed status = %d", (int)status);
}
// 从sps pps中获取解码视频的宽高信息
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(_decoderFormatDescription);
// kCVPixelBufferPixelFormatTypeKey 解码图像的采样格式
// kCVPixelBufferWidthKey、kCVPixelBufferHeightKey 解码图像的宽高
// kCVPixelBufferOpenGLCompatibilityKey制定支持OpenGL渲染,经测试有没有这个参数好像没什么差别
NSDictionary* destinationPixelBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange), (id)kCVPixelBufferWidthKey : @(dimensions.width), (id)kCVPixelBufferHeightKey : @(dimensions.height),
(id)kCVPixelBufferOpenGLCompatibilityKey : @(YES)};
// 设置解码输出数据回调
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = decodeOutputDataCallback;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
// 创建解码器
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decoderFormatDescription, NULL, (__bridge CFDictionaryRef)destinationPixelBufferAttributes, &callBackRecord, &_deocderSession);
// 解码线程数量
VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_ThreadCount, (__bridge CFTypeRef)@(1));
// 是否实时解码
VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_RealTime, kCFBooleanTrue);
return YES;
}
/**
解码数据
@param frame 数据
@param frameSize 数据长度
*/
-(void)decode:(uint8_t *)frame withSize:(uint32_t)frameSize
{
CMBlockBufferRef blockBuffer = NULL;
// 创建 CMBlockBufferRef
OSStatus status = CMBlockBufferCreateWithMemoryBlock(NULL, (void *)frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, FALSE, &blockBuffer);
if(status != kCMBlockBufferNoErr)
{
return;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
// 创建 CMSampleBufferRef
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decoderFormatDescription , 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != kCMBlockBufferNoErr || sampleBuffer == NULL)
{
return;
}
// VTDecodeFrameFlags 0为允许多线程解码
VTDecodeFrameFlags flags = 0;
VTDecodeInfoFlags flagOut = 0;
// 解码 这里第四个参数会传到解码的callback里的sourceFrameRefCon,可为空
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_deocderSession, sampleBuffer, flags, NULL, &flagOut);
if(decodeStatus == kVTInvalidSessionErr)
{
NSLog(@"H264Decoder::Invalid session, reset decoder session");
}
else if(decodeStatus == kVTVideoDecoderBadDataErr)
{
NSLog(@"H264Decoder::decode failed status = %d(Bad data)", (int)decodeStatus);
}
else if(decodeStatus != noErr)
{
NSLog(@"H264Decoder::decode failed status = %d", (int)decodeStatus);
}
// Create了就得Release
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
return;
}
//解码回调函数
static void decodeOutputDataCallback(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration)
{
// retain再输出,外层去release;pixelBuffer就是解码后的数据
CVPixelBufferRetain(pixelBuffer);
Nextvc *decoder = (__bridge Nextvc *)decompressionOutputRefCon;
}
CMSampleBufferRef转换成YUV数据、YUV数据类型的变换:
/*
1. CMSampleBufferRef 中提取yuv数据(Byte)
2. 处理yuv数据
3. yuv数据 转CVPixelBufferRef ,继续进行编码
*/
-(CVPixelBufferRef)processYUV422ToYUV420WithSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
// 1. 从CMSampleBufferRef中提取yuv数据
// 获取yuv数据
// 通过CMSampleBufferGetImageBuffer方法,获得CVImageBufferRef。
// 这里面就包含了yuv420数据的指针
CVImageBufferRef pixelBuffer_Before = CMSampleBufferGetImageBuffer(sampleBuffer);
//表示开始操作数据
CVPixelBufferLockBaseAddress(pixelBuffer_Before, 0);
//图像宽度(像素)
size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer_Before);
//图像高度(像素)
size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer_Before);
//yuv中的y所占字节数
size_t y_size = pixelWidth * pixelHeight;
// 2. yuv中的u和v分别所占的字节数
size_t uv_size = y_size / 4;
uint8_t *yuv_frame = malloc(uv_size * 2 + y_size);
//获取CVImageBufferRef中的y数据
uint8_t *y_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer_Before, 0);
memcpy(yuv_frame, y_frame, y_size);
//获取CMVImageBufferRef中的uv数据
uint8_t *uv_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer_Before, 1);
memcpy(yuv_frame + y_size, uv_frame, uv_size * 2);
CVPixelBufferUnlockBaseAddress(pixelBuffer_Before, 0);
NSData *yuvData = [NSData dataWithBytesNoCopy:yuv_frame length:y_size + uv_size * 2];
// 3. yuv 变成 转CVPixelBufferRef
//现在要把NV12数据放入 CVPixelBufferRef中,因为 硬编码主要调用VTCompressionSessionEncodeFrame函数,此函数不接受yuv数据,但是接受CVPixelBufferRef类型。
CVPixelBufferRef pixelBuf_After = NULL;
//初始化pixelBuf,数据类型是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,此类型数据格式同NV12格式相同。
CVPixelBufferCreate(NULL,
pixelWidth, pixelHeight,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
NULL,
&pixelBuf_After);
// Lock address,锁定数据,应该是多线程防止重入操作。
if(CVPixelBufferLockBaseAddress(pixelBuf_After, 0) != kCVReturnSuccess){
NSLog(@"encode video lock base address failed");
return NULL;
}
//将yuv数据填充到CVPixelBufferRef中
uint8_t *yuv_frame_2 = (uint8_t *)yuvData.bytes;
//处理y frame
uint8_t *y_frame_2 = CVPixelBufferGetBaseAddressOfPlane(pixelBuf_After, 0);
memcpy(y_frame_2, yuv_frame_2, y_size);
uint8_t *uv_frame_2 = CVPixelBufferGetBaseAddressOfPlane(pixelBuf_After, 1);
memcpy(uv_frame_2, yuv_frame_2 + y_size, uv_size * 2);
CVPixelBufferUnlockBaseAddress(pixelBuf_After, 0);
return pixelBuf_After;
}
软硬编码对比:
软编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
硬编码:性能高,低码率下通常质量低于软编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能,不过Mac OS系统一直有,被称为Video ToolBox的框架来处理硬件的编码和解码,终于在iOS 8.0后,苹果将该框架引入iOS系统。
**********.H265优点
- 压缩比高,在相同图片质量情况下,比JPEG高两倍
- 能增加如图片的深度信息,透明通道等辅助图片。
- 支持存放多张图片,类似相册和集合。(实现多重曝光的效果)
- 支持多张图片实现GIF和livePhoto的动画效果。
- 无类似JPEG的最大像素限制
- 支持透明像素
- 分块加载机制
- 支持缩略图
*********在iOS平台上做视频的解码,一般有三种方案:
1、软解码方案:ffmpeg
缺点:消耗CPU太大,在iphone4s上一般720P 20帧以上就解不动了
2、硬解码方案1:采用私有接口VideoToolBox
优点:CPU消耗极低,解码效率极高
缺点:要使用私有接口VideoToolBox
3、硬解码方案2:采用AVPlayer+httpserver+HttpLiveStream的组合方案
优点:CPU消耗极低,解码效率极高
缺点:视频有延迟,不适合实时视频通讯