現(xiàn)在的高清手機拍個照都有2M以上,按照人眼幀率24幀/秒的速度傳輸,網(wǎng)絡(luò)數(shù)度需要達到2 * 24M/秒,一般日常中的網(wǎng)絡(luò)顯然不可能有這樣的網(wǎng)速。這時就需要對音視頻進行編碼壓縮了。
常用的編碼類型有:
- 視頻編碼:H.264、H.265、VP8、VP9
- 音頻編碼:aac、Opus、mp3
生活中常說的mp4、avi、flv等指的是封裝格式,就是個容器,把音視頻、字幕、媒體信息等裝進容器里,編碼在這里充當?shù)氖菈嚎s音視頻的角色,這樣才能減少體積。
名詞介紹
YUV
視頻裸數(shù)據(jù)的一種格式,大部分設(shè)備的視頻幀數(shù)據(jù)都是YUV,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用于指定像素的顏色。跟我們熟悉的RGB類似,YUV也是一種顏色編碼方法。
主要用于電視系統(tǒng)以及模擬視頻領(lǐng)域,它將亮度信息(Y)與色彩信息(UV)分離,沒有UV信息一樣可以顯示完整的圖像,只不過是黑白的,這樣的設(shè)計很好地解決了彩色電視機與黑白電視的兼容問題。并且,YUV不像RGB那樣要求三個獨立的視頻信號同時傳輸,所以用YUV方式傳送占用極少的頻寬。
NV12和NV21屬于YUV420格式,是一種two-plane模式,蘋果iOS的視頻是NV12格式,Android的視頻是NV21格式
H.264
拍攝一段視頻,如果全身基本不動,只有嘴唇在動,如果記錄每一幀的畫面那么這段視頻會非常大,如果只記錄每個幀之間的變化,觀看視頻每幀的原始數(shù)據(jù)都可以在上一幀的基礎(chǔ)上加上這一幀的變化數(shù)據(jù)還原出來,不僅大幅度縮小體積,還能保持比較好的畫質(zhì),整個過程就是H.264的編碼、解碼
在H264協(xié)議里定義了三種幀:
I幀:完整編碼的幀,也叫關(guān)鍵幀
P幀:參考之前的I幀生成的只包含差異部分編碼的幀
B幀:參考前后的幀編碼的幀叫B幀
H264采用的核心算法是幀內(nèi)壓縮和幀間壓縮,幀內(nèi)壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
H264碼流可以分為兩層:VCL層和NAL層
NAL:Network abstraction layer,叫網(wǎng)絡(luò)抽象層,它保存了H264相關(guān)的參數(shù)信息和圖像信息,NAL層由多個單元NALU組成,NALU由NALU頭(00 00 00 01或者00 00 01)、sps(序列參數(shù)集)、pps(圖像參數(shù)集合)、slice、sei、IDR幀、I幀(在圖像運動變化較少時,I幀后面是7個P幀,如果圖像運動變化大時,一個序列就短了,I幀后面可能是3個或者4個P幀)、P幀、B幀等數(shù)據(jù)。
H264原始碼流是由一個接一個的NALU(Nal Unit)組成的,NALU = 開始碼 + NAL類型 + 視頻數(shù)據(jù)
開始碼用于標示這是一個NALU 單元的開始,必須是"00 00 00 01" 或"00 00 01"
NALU類型如下:
| 類型 | 說明 |
|---|---|
| 0 | 未規(guī)定 |
| 1 | 非IDR圖像中不采用數(shù)據(jù)劃分的片段 |
| 2 | 非IDR圖像中A類數(shù)據(jù)劃分片段 |
| 3 | 非IDR圖像中B類數(shù)據(jù)劃分片段 |
| 4 | 非IDR圖像中C類數(shù)據(jù)劃分片段 |
| 5 | IDR圖像的片段 |
| 6 | 補充增強信息(SEI) |
| 7 | 序列參數(shù)集(SPS) |
| 8 | 圖像參數(shù)集(PPS) |
| 9 | 分割符 |
| 10 | 序列結(jié)束符 |
| 11 | 流結(jié)束符 |
| 12 | 填充數(shù)據(jù) |
| 13 | 序列參數(shù)集擴展 |
| 14 | 帶前綴的NAL單元 |
| 15 | 子序列參數(shù)集 |
| 16-18 | 保留 |
| 19 | 不采用數(shù)據(jù)劃分的輔助編碼圖像片段 |
| 20 | 編碼片段擴展 |
| 21-23 | 保留 |
| 24-31 | 未規(guī)定 |
一般我們只用到了1、5、7、8這4個類型就夠了。類型為5表示這是一個I幀,I幀前面必須有SPS和PPS數(shù)據(jù),也就是類型為7和8,類型為1表示這是一個P幀或B幀。
GOP
Group of picture圖像組,也就是兩個I幀之間的距離,GOP值越大,那么I幀率之間P幀和B幀數(shù)量越多,圖像畫質(zhì)越精細,如果GOP是120,如果分辨率是720P,幀率是60,那么兩I幀的時間就是120/60=2s.
SPS
sequence parameter set,序列參數(shù)集(解碼相關(guān)信息,檔次級別、分別率、某檔次中編碼工具開關(guān)標識和涉及的參數(shù)、時域可分級信息等)
PPS
picture parameter set, 圖像參數(shù)集(一幅圖像所用的公共參數(shù),一幅圖像中所有SS應(yīng)用同一個PPS,初始圖像控制信息,初始化參數(shù)、分塊信息)
IDR
Instantaneous Decoding Refresh, 即時解碼刷新,跟I幀是同一個東西,在編碼和解碼中為了方便,要首個I幀和其他I幀區(qū)別開,所以才把第一個首個I幀叫IDR,這樣就方便控制編碼和解碼流程。
H.265
比H.264更加高效的編碼方式是H.265,是H.264的下一個版本,它屬于下一代MPEG-H標準而非MPEG-4,比H264有著更強的壓縮效率,被稱為HEVC(High Efficiency Video Coding/高效視頻編碼),同碼率下理論占用空間節(jié)省了50%足足一半,動態(tài)畫面表現(xiàn)會更加清晰。
- 市面上,雖然已經(jīng)有一些新手機、電視、CPU都支持硬解H265了,但更多的現(xiàn)存老設(shè)備都不能向上支持硬解,軟解更吃不消。
- H265的商業(yè)授權(quán)費太貴,市場都沒成熟呢,網(wǎng)絡(luò)視頻站點更不樂意花大錢推。但國內(nèi)IDC帶寬成本比較高,能降低一半的流量,無論對我們用戶,還是對運營商,都是很香的,相信再過幾年,H265就會很快普及了。
PCM
原始的未經(jīng)壓縮的音頻采樣數(shù)據(jù)裸流,它是由模擬信號經(jīng)過采樣、量化、編碼轉(zhuǎn)換成的標準數(shù)字音頻數(shù)據(jù)。
描述PCM數(shù)據(jù)的6個參數(shù):
- Sample Rate : 采樣頻率。8kHz(電話)、44.1kHz(CD)、48kHz(DVD)。
- Sample Size : 量化位數(shù)。通常該值為16-bit。
- Number of Channels : 通道個數(shù)。常見的音頻有立體聲(stereo)和單聲道(mono)兩種類型,立體聲包含左聲道和右聲道。另外還有環(huán)繞立體聲等其它不太常用的類型。
- Sign : 表示樣本數(shù)據(jù)是否是有符號位,比如用一字節(jié)表示的樣本數(shù)據(jù),有符號的話表示范圍為-128 ~ 127,無符號是0 ~ 255。
- Byte Ordering : 字節(jié)序。字節(jié)序是little-endian還是big-endian。通常均為little-endian。字節(jié)序說明見第4節(jié)。
- Integer Or Floating Point : 整形或浮點型。大多數(shù)格式的PCM樣本數(shù)據(jù)使用整形表示,而在一些對精度要求高的應(yīng)用方面,使用浮點類型表示PCM樣本數(shù)據(jù)。
AAC
全稱Advanced Audio Coding,高級音頻編碼,是一種專為聲音數(shù)據(jù)設(shè)計的文件壓縮格式。與MP3不同,它采用了全新的算法進行編碼,更加高效,具有更高的“性價比”。利用AAC格式,可使人感覺聲音質(zhì)量沒有明顯降低的前提下,更加小巧。
優(yōu)點:相較于mp3,AAC格式的音質(zhì)更佳,文件更小。
不足:AAC屬于有損壓縮的格式,與時下流行的APE、FLAC等無損格式相比音質(zhì)存在“本質(zhì)上”的差距。加之,傳輸速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC頭上“小巧”的光環(huán)不復(fù)存在。
幀率
單位為fps(frame pre second),視頻畫面每秒有多少幀畫面,數(shù)值越大畫面越流暢
碼率
單位為bps(bit pre second),視頻每秒輸出的數(shù)據(jù)量,數(shù)值越大畫面越清晰
分辨率
視頻畫面像素密度,例如常見的720P、1080P等
關(guān)鍵幀間隔
每隔多久編碼一個關(guān)鍵幀
流程:

視頻編碼
分為軟編碼和音編碼方式
- 軟編碼
使用CPU進行編碼。
H.264 初始化軟編碼:
- (void)initCompressionSession{
_sendQueue = dispatch_queue_create("com.youku.laifeng.h264.sendframe", DISPATCH_QUEUE_SERIAL);
[self initializeNALUnitStartCode];
_lastPTS = kCMTimeInvalid;
_timescale = 1000;
frameCount = 0;
#ifdef DEBUG
enabledWriteVideoFile = NO;
[self initForFilePath];
#endif
_encoder = [WSAVEncoder encoderForHeight:(int)_configuration.videoSize.height andWidth:(int)_configuration.videoSize.width bitrate:(int)_configuration.videoBitRate];
[_encoder encodeWithBlock:^int(NSArray* dataArray, CMTimeValue ptsValue) {
[self incomingVideoFrames:dataArray ptsValue:ptsValue];
return 0;
} onParams:^int(NSData *data) {
[self generateSPSandPPS];
return 0;
}];
}
H.264開始軟編碼:
- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
CMVideoFormatDescriptionRef videoInfo = NULL;
CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);
CMTime frameTime = CMTimeMake(timeStamp, 1000);
CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
CMSampleTimingInfo timing = {duration, frameTime, kCMTimeInvalid};
CMSampleBufferRef sampleBuffer = NULL;
CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, YES, NULL, NULL, videoInfo, &timing, &sampleBuffer);
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
[_encoder encodeFrame:sampleBuffer];
CFRelease(videoInfo);
CFRelease(sampleBuffer);
frameCount++;
}
- 硬編碼
不使用CPU進行編碼,使用顯卡(GPU)進行硬件加速,專用的DSP、FPGA、ASIC芯片等硬件進行編碼。
iOS8.0以上才支持硬編碼
H.264初始化硬編碼:
- (void)resetCompressionSession {
if (compressionSession) {
VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(compressionSession);
CFRelease(compressionSession);
compressionSession = NULL;
}
// allocator 分配器,設(shè)置為默認分配
// width 寬
// height 高
// encoderSpecification 編碼規(guī)范,設(shè)置nil由videoToolbox自己選擇
// imageBufferAttributes 源像素緩沖區(qū)屬性.設(shè)置nil不讓videToolbox創(chuàng)建,而自己創(chuàng)建
// compressedDataAllocator 壓縮數(shù)據(jù)分配器.設(shè)置nil,默認的分配
// outputCallback 編碼回調(diào)
// refcon 回調(diào)客戶定義的參考值,此處把self傳過去,因為我們需要在C函數(shù)中調(diào)用self的方法,而C函數(shù)無法直接調(diào)self
// compressionSessionOut 編碼會話
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;
/// 設(shè)置關(guān)鍵幀(GOPsize)間隔,GOP太小的話圖像會模糊
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
/// 最大幀率間隔
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval/_configuration.videoFrameRate));
/// 設(shè)置期望幀率,不是實際幀率
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_configuration.videoFrameRate));
/// 碼率,碼率大了話就會非常清晰,但同時文件也會比較大。碼率小的話,圖像有時會模糊,但也勉強能看
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_configuration.videoBitRate));
/// 數(shù)據(jù)率限制
NSArray *limit = @[@(_configuration.videoBitRate * 1.5/8), @(1)];
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
/// 設(shè)置實時編碼輸出(避免延遲)
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
/// 是否產(chǎn)生B幀(因為B幀在解碼時并不是必要的,是可以拋棄B幀的)
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC);
VTCompressionSessionPrepareToEncodeFrames(compressionSession);
}
H.264開始編碼:
- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
if(_isBackGround) return;
frameCount++;
CMTime presentationTimeStamp = CMTimeMake(frameCount, (int32_t)_configuration.videoFrameRate);
VTEncodeInfoFlags flags;
CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
NSDictionary *properties = nil;
if (frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0) {
properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
}
NSNumber *timeNumber = @(timeStamp);
OSStatus status = VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
if(status != noErr){
[self resetCompressionSession];
}
}
H.265敬請期待。。。。。。。。!
音頻編碼
AAC編碼:
- (void)encodeAudioData:(nullable NSData*)audioData timeStamp:(uint64_t)timeStamp {
if (![self createAudioConvert]) {
return;
}
if(leftLength + audioData.length >= self.configuration.bufferLength){
/// 發(fā)送
NSInteger totalSize = leftLength + audioData.length;
NSInteger encodeCount = totalSize/self.configuration.bufferLength;
char *totalBuf = malloc(totalSize);
char *p = totalBuf;
memset(totalBuf, (int)totalSize, 0);
memcpy(totalBuf, leftBuf, leftLength);
memcpy(totalBuf + leftLength, audioData.bytes, audioData.length);
for(NSInteger index = 0;index < encodeCount;index++){
[self encodeBuffer:p timeStamp:timeStamp];
p += self.configuration.bufferLength;
}
leftLength = totalSize%self.configuration.bufferLength;
memset(leftBuf, 0, self.configuration.bufferLength);
memcpy(leftBuf, totalBuf + (totalSize -leftLength), leftLength);
free(totalBuf);
}else{
/// 積累
memcpy(leftBuf+leftLength, audioData.bytes, audioData.length);
leftLength = leftLength + audioData.length;
}
}
- (void)encodeBuffer:(char*)buf timeStamp:(uint64_t)timeStamp{
AudioBuffer inBuffer;
inBuffer.mNumberChannels = 1;
inBuffer.mData = buf;
inBuffer.mDataByteSize = (UInt32)self.configuration.bufferLength;
AudioBufferList buffers;
buffers.mNumberBuffers = 1;
buffers.mBuffers[0] = inBuffer;
// 初始化編碼后輸出緩沖列表
AudioBufferList outBufferList;
outBufferList.mNumberBuffers = 1;
outBufferList.mBuffers[0].mNumberChannels = inBuffer.mNumberChannels;
outBufferList.mBuffers[0].mDataByteSize = inBuffer.mDataByteSize; // 設(shè)置緩沖區(qū)大小
outBufferList.mBuffers[0].mData = aacBuf; // 設(shè)置AAC緩沖區(qū)
UInt32 outputDataPacketSize = 1;
if (AudioConverterFillComplexBuffer(m_converter, inputDataProc, &buffers, &outputDataPacketSize, &outBufferList, NULL) != noErr) {
return;
}
WSAudioFrame *audioFrame = [WSAudioFrame new];
audioFrame.timestamp = timeStamp;
audioFrame.data = [NSData dataWithBytes:aacBuf length:outBufferList.mBuffers[0].mDataByteSize];
char exeData[2];
exeData[0] = _configuration.asc[0];
exeData[1] = _configuration.asc[1];
audioFrame.audioInfo = [NSData dataWithBytes:exeData length:2];
if (self.aacDeleage && [self.aacDeleage respondsToSelector:@selector(audioEncoder:audioFrame:)]) {
[self.aacDeleage audioEncoder:self audioFrame:audioFrame];
}
if (self->enabledWriteVideoFile) {
NSData *adts = [self adtsData:_configuration.numberOfChannels rawDataLength:audioFrame.data.length];
fwrite(adts.bytes, 1, adts.length, self->fp);
fwrite(audioFrame.data.bytes, 1, audioFrame.data.length, self->fp);
}
}