前言
好記性不如爛筆頭,最近有點空把一些知識也整理了一遍,后面陸續寫一些總結吧!先從這個不太熟悉的音視頻這塊開始吧,2016年可謂是直播元年,這塊的技術也沒什么很陌生了,后知后覺的自己最近也開始學習學習,有挺多調用 C 函數,如果是進行編碼封裝格式最好還是用c語言寫跨平臺且效率也更佳,后面有時間嘗試用c寫下,此文只是直播技術的一部分,未完待續,如有誤歡迎指正,當然如果看完覺得有幫助也請幫忙點個喜歡??。
技術整體概覽
-
直播的技術
- 直播技術總體來說分為采集、前處理、編碼、傳輸、解碼、渲染這幾個環節
-
流程圖例:
直播流程.jpeg
音視頻采集
采集介紹
音視頻的采集是直播架構的第一個環節,也是直播的視頻來源,視頻采集有多個應用場景,比如二維碼開發。音視頻采集包括兩部分:
視頻采集
音頻采集
iOS 開發中,是可以同步采集音視頻,相關的采集 API 封裝在 AVFoundation 框架中,使用方式簡單
采集步驟
- 導入框架 AVFoundation 框架
- 創建捕捉會話(AVCaptureSession)
- 該會話用于連接之后的輸入源&輸出源
- 輸入源:攝像頭&話筒
- 輸出源:拿到對應的音頻&視頻數據的出口
- 會話:用于將輸入源&輸出源連接起來
- 設置視頻輸入源&輸出源相關屬性
- 輸入源(AVCaptureDeviceInput):從攝像頭輸入
- 輸出源(AVCaptureVideoDataOutput):可以設置代理,在代理中處理對應輸入后得到的數據,以及設置例如丟幀等情況的處理
- 將輸入&輸出添加到會話中
采集代碼
- 自定義一個繼承
NSObject
的CCVideoCapture
類,用用于處理音視頻的采集 -
CCVideoCapture
代碼如下:
//
// CCVideoCapture.m
// 01.視頻采集
//
// Created by zerocc on 2017/3/29.
// Copyright ? 2017年 zerocc. All rights reserved.
//
#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>
@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@end
@implementation CCVideoCapture
- (void)startCapturing:(UIView *)preView
{
// =============== 采集視頻 ===========================
// 1. 創建 session 會話
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPreset1280x720;
self.session = session;
// 2. 設置音視頻的輸入
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];
[self.session addInput:videoInput];
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
[self.session addInput:audioInput];
// 3. 設置音視頻的輸出
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
dispatch_queue_t videoQueue = dispatch_queue_create("Video Capture Queue", DISPATCH_QUEUE_SERIAL);
[videoOutput setSampleBufferDelegate:self queue:videoQueue];
[videoOutput setAlwaysDiscardsLateVideoFrames:YES];
if ([session canAddOutput:videoOutput]) {
[self.session addOutput:videoOutput];
}
AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
dispatch_queue_t audioQueue = dispatch_queue_create("Audio Capture Queue", DISPATCH_QUEUE_SERIAL);
[audioOutput setSampleBufferDelegate:self queue:audioQueue];
if ([session canAddOutput:audioOutput]) {
[session addOutput:audioOutput];
}
// 4. 獲取視頻輸入與輸出連接,用于分辨音視頻數據
// 視頻輸出方向 默認方向是相反設置方向,必須在將 output 添加到 session 之后
AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
self.videoConnection = videoConnection;
if (videoConnection.isVideoOrientationSupported) {
videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
[self.session commitConfiguration];
}else {
NSLog(@"不支持設置方向");
}
// 5. 添加預覽圖層
AVCaptureVideoPreviewLayer *layer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
self.layer = layer;
layer.frame = preView.bounds;
[preView.layer insertSublayer:layer atIndex:0];
// 6. 開始采集
[self.session startRunning];
}
// 丟幀視頻情況
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (self.videoConnection == connection) {
dispatch_sync(queue, ^{
});
NSLog(@"采集到視頻數據");
} else {
dispatch_sync(queue, ^{
});
NSLog(@"采集到音頻數據");
}
NSLog(@"采集到視頻畫面");
}
- (void)stopCapturing
{
[self.encoder endEncode];
[self.session stopRunning];
[self.layer removeFromSuperlayer];
}
@end
音視頻編碼
音視頻壓縮編碼介紹
-
不經編碼的視頻非常龐大,存儲起來都麻煩,更何況網絡傳輸
- 編碼通過壓縮音視頻數據來減少數據體積,方便音視頻數據的推流、拉流和存儲,能大大提高存儲傳輸效率
- 以錄制視頻一秒鐘為例,需要裸流視頻多大空間呢?
- 視頻無卡頓效果一秒鐘至少16幀(正常開發一般 30FPS )
- 假如該視頻是一個1280*720分辨率的視頻
- 對應像素用RGB,3*8
- 結果多大:(1280 x 720 x 30 x 24) / (1024 x 1024) = 79MB
-
錄制一秒鐘音頻,又需要多大空間?
- 取樣頻率若為44.1KHz
- 樣本的量化比特數為16
- 普通立體聲的信號通道數為2
- 音頻信號的傳輸率 = 取樣頻率 x 樣本的量化比特數 x 通道數;那么計算一秒鐘音頻數據量大概為1.4MB
音視頻中的冗余信息
視頻(裸流)存在冗余信息,空間冗余、時間冗余、視覺冗余等等,經過一系列的去除冗余信息,可以大大的降低視頻的數據量,更利用視頻的保存、傳輸。去除冗余信息的過程,我們就稱之為視頻圖像壓縮編碼。
數字音頻信號包含了對人感受可以忽略的冗余信息,時域冗余、頻域冗余、聽覺冗余等等。
-
音視頻編碼方式:
- 硬編碼:使用非 CPU 進行編碼,如利用系統提供的顯卡 GPU、專用 DSP 芯片等
- 軟編碼:使用 CPU 進行編碼(手機容易發熱)
-
各個平臺處理:
- iOS端:硬編碼兼容性較好,可以直接進行硬編碼,蘋果在iOS 8.0后,開放系統的硬件編碼解碼功能,Video ToolBox 的框架來處理硬件的編碼和解碼,蘋果將該框架引入iOS系統。
- Android端:硬編碼較難,難找到統一的庫兼容各個平臺(推薦使用軟編碼)
-
編碼標準:
視頻編碼:H.26X 系列、MPEG 系列(由ISO[國際標準組織機構]下屬的MPEG[運動圖像專家組]開發)、C-1、VP8、VP9等
H.261:主要在老的視頻會議和視頻電話產品中使用
H.263:主要用在視頻會議、視頻電話和網絡視頻上
H.264:H.264/MPEG-4 第十部分。或稱AVC (Advanced Video Coding, 高級視頻編碼),是一種視頻壓縮標準,一種被廣泛使用的高精度視頻錄制、壓縮和發布格式。
H.265:高效率視頻編碼(High Efficency Video Coding, 簡稱HEVC) 是一種視頻壓縮標準,H.264/MPEG-4 AVC 的繼承者。可支持4K 分辨率甚至到超高畫質電視,最高分辨率可達到8192*4320(8K分辨率),這是目前發展的趨勢,尚未有大眾化編碼軟件出現
MPEG-1第二部分:MPEG-1第二部分主要使用在VCD 上,有寫在有線視頻也是用這種格式
MPEG-2第二部分:MPEG-2第二部分等同于H.262,使用在DVD、SVCD和大多數數字視頻廣播系統中
MPEG-4第二部分:MPEG-4第二部分標準可以使用在網絡傳輸、廣播和媒體存儲上
音頻編碼:AAC、Opus、MP3、AC-3等
視頻壓縮編碼 H.264 (AVC)
-
序列(GOP - Group of picture)
- 在H264中圖像以序列為單位進行組織,一個序列是一段圖像編碼后的數據流
- 一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是I幀圖像
- H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列
- 這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會
- IDR 圖像之后的圖像永遠不會使用 IDR 之前的圖像的數據來解碼
- 一個序列就是一段內容差異不太大的圖像編碼后生成的一串數據流
- 當運動變化較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的內容變動很小,所以就可以編一個I幀,I幀做為隨機訪問的參考點,然后一直P幀、B幀了
- 當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀
-
在H264協議里定義了三種幀
- I幀:完整編碼的叫做I幀,關鍵幀
- P幀:參考之前的I幀生成的只包含差異部分編碼的幀叫做P幀
- B幀:參考前后的幀(I&P)編碼的幀叫B幀
-
H264的壓縮方法
- 分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化幀數不宜取多
- 定義幀:將每組內容幀圖像定義為三種類型:既I幀、P幀、B幀
- 預測幀:以I幀做為基礎幀,以I幀預測P幀,再由P幀預測B幀
- 數據傳輸:最后將I幀數據與預測的差值信息進行存儲和傳輸
- H264采用的核心算法是幀內壓縮和幀間壓縮
- 幀內壓縮也稱為空間壓縮是生成I幀的算法,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。
- 幀間壓縮也稱為時間壓縮是生成P幀和B幀的算法,它通過比較時間軸上不同幀之間的數據進行壓縮,通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
-
分層設計
- H264算法在概念上分為兩層:視頻編碼層(VCL:Video Coding Layer)負責高效的視頻內容表示;網絡提取層(NAL:Network Abstraction Layer)負責以網絡所要求的恰當的方式對數據進行打包和傳送。這樣,高效編碼和網絡友好性分別VCL 和 NAL 分別完成
- NAL設計目的,根據不同的網絡把數據打包成相應的格式,將VCL產生的比特字符串適配到各種各樣的網絡和多元環境中
-
NAL 的封裝方式
H.264流數據正是由一系列的 NALU 單元(NAL Unit, 簡稱NALU)組成的。
NALU 是將每一幀數據寫入到一個 NALU 單元中,進行傳輸或存儲的
NALU 分為 NALU 頭和 NALU 體
NALU 頭通常為00 00 00 01,作為要給新的 NALU 的起始標識
-
NALU體封裝著VCL編碼后的信息或者其他信息
h264碼流結構.png
-
封裝過程
- 對于流數據來說,一個NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭,
0x00 00 01因此被稱為開始碼(Start code)。 - 提取 SPS (Picture Parameter Sets) 圖像參數集和 PPS (Sequence Parameter Set)序列參數集 生成 FormatDesc 非VCL的NAL單元,一般來說編碼器編出的首幀數據為 PPS 與 SPS 。
- 每個NALU的開始碼是0x00 00 01,按照開始碼定位NALU
- 通過類型信息找到sps和pps并提取,開始碼后第一個byte的后5位,7代表sps,8代表pps
- 使用
CMSampleBufferGetFormatDescription
函數來構建CMVideoFormatDescriptionRef
- 對于流數據來說,一個NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭,
提取視頻圖像數據生成
CMBlockBuffer
,編碼后的I幀、后續B幀、P幀數據自定義一個
CCH264Encoder
類處理 視頻(裸流) h264 硬編碼,代碼如下:
//
// CCH264Encoder.h
// 01.視頻采集
//
// Created by zerocc on 2017/4/4.
// Copyright ? 2017年 zerocc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
@interface CCH264Encoder : NSObject
- (void)prepareEncodeWithWidth:(int)width height:(int)height;
- (void)encoderFram:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end
//
// CCH264Encoder.m
// 01.視頻采集
//
// Created by zerocc on 2017/4/4.
// Copyright ? 2017年 zerocc. All rights reserved.
//
#import "CCH264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
#import <AudioToolbox/AudioToolbox.h>
@interface CCH264Encoder () {
int _spsppsFound;
}
@property (nonatomic, assign) VTCompressionSessionRef compressionSession; // coreFoundation中的對象 == c語言對象 不用*,strong只用來修飾oc對象,VTCompressionSessionRef對象本身就是指針 用assign修飾
@property (nonatomic, assign) int frameIndex;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@property (nonatomic, strong) NSString *documentDictionary;
@end
@implementation CCH264Encoder
#pragma mark - lazyload
- (NSFileHandle *)fileHandle {
if (!_fileHandle) {
// 這里只寫一個裸流,只有視頻流沒有音頻流
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) firstObject] stringByAppendingPathComponent:@"video.h264"];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isExistPath = [fileManager isExecutableFileAtPath:filePath];
if (isExistPath) {
[fileManager removeItemAtPath:filePath error:nil];
}
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
}
return _fileHandle;
}
/**
準備編碼
@param width 采集的寬度
@param height 采集的高度
*/
- (void)prepareEncodeWithWidth:(int)width height:(int)height
{
// 0. 設置默認是第0幀
self.frameIndex = 0;
// 1. 創建 VTCompressionSessionRef
/**
VTCompressionSessionRef
@param NULL CFAllocatorRef - CoreFoundation分配內存的模式,NULL默認
@param width int32_t - 視頻寬度
@param height 視頻高度
@param kCMVideoCodecType_H264 CMVideoCodecType - 編碼的標準
@param NULL CFDictionaryRef encoderSpecification,
@param NULL CFDictionaryRef sourceImageBufferAttributes
@param NULL CFAllocatorRef compressedDataAllocator
@param didComparessionCallback VTCompressionOutputCallback - 編碼成功后的回調函數c函數
@param _Nullable void * - 傳遞到回調函數中參數
@return session
*/
VTCompressionSessionCreate(NULL,
width,
height,
kCMVideoCodecType_H264,
NULL, NULL, NULL,
didComparessionCallback,
(__bridge void * _Nullable)(self),
&_compressionSession);
// 2. 設置屬性
// 2.1 設置實時編碼
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_RealTime,
kCFBooleanTrue);
// 2.2 設置幀率
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_ExpectedFrameRate,
(__bridge CFTypeRef _Nonnull)(@24));
// 2.3 設置比特率(碼率) 1500000/s
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_AverageBitRate,
(__bridge CFTypeRef _Nonnull)(@1500000)); // 每秒有150萬比特 bit
// 2.4 關鍵幀最大間隔,也就是I幀。
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_DataRateLimits,
(__bridge CFTypeRef _Nonnull)(@[@(1500000/8), @1])); // 單位是 8 byte
// 2.5 設置GOP的大小
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_MaxKeyFrameInterval,
(__bridge CFTypeRef _Nonnull)(@20));
// 3. 準備編碼
VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
}
/**
開始編碼
@param sampleBuffer CMSampleBufferRef
*/
- (void)encoderFram:(CMSampleBufferRef)sampleBuffer
{
// 2. 開始編碼
// 將CMSampleBufferRef 轉換成 CVImageBufferRef,
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime pts = CMTimeMake(self.frameIndex, 24);
CMTime duration = kCMTimeInvalid;
VTEncodeInfoFlags flags;
/**
硬編碼器 - 將攜帶視頻幀數據的imageBuffer送到硬編碼器中,進行編碼。
@param session#> VTCompressionSessionRef
@param imageBuffer#> CVImageBufferRef
@param presentationTimeStamp#> CMTime - pts
@param duration#> CMTime - 傳一個固定的無效的時間
@param frameProperties#> CFDictionaryRef
@param sourceFrameRefCon#> void - 編碼后回調函數中第二個參數
@param infoFlagsOut#> VTEncodeInfoFlags - 編碼后回調函數中第四個參數
@return
*/
VTCompressionSessionEncodeFrame(self.compressionSession,
imageBuffer,
pts,
duration,
NULL,
NULL,
&flags);
NSLog(@"開始編碼一幀數據");
}
#pragma mark - 獲取編碼后的數據 c語言函數 - 編碼后的回調函數
void didComparessionCallback (void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer)
{
// c語言中不能調用當前對象self.語法不行,只有通過指針去做相應操作
// 獲取當前 CCH264Encoder 對象,通過傳入的 self 參數(VTCompressionSessionCreate中傳入了self)
CCH264Encoder *encoder = (__bridge CCH264Encoder *)(outputCallbackRefCon);
// 1. 判斷該幀是否為關鍵幀
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
BOOL isKeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
// 2. 如果是關鍵幀 -> 獲取 SPS/PPS 數據 其保存了h264視頻的一些必要信息方便解析 -> 并且寫入文件
if (isKeyFrame && !encoder->_spsppsFound) {
encoder->_spsppsFound = 1;
// 2.1. 從CMSampleBufferRef獲取CMFormatDescriptionRef
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
// 2.2. 獲取 SPS /pps的信息
const uint8_t *spsOut;
size_t spsSize, spsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
0,
&spsOut,
&spsSize,
&spsCount,
NULL);
const uint8_t *ppsOut;
size_t ppsSize, ppsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
1,
&ppsOut,
&ppsSize,
&ppsCount,
NULL);
// 2.3. 將SPS/PPS 轉成NSData,并且寫入文件
NSData *spsData = [NSData dataWithBytes:spsOut length:spsSize];
NSData *ppsData = [NSData dataWithBytes:ppsOut length:ppsSize];
// 2.4. 寫入文件 (NALU單元特點:起始都是有 0x00 00 00 01 每個NALU需拼接)
[encoder writeH264Data:spsData];
[encoder writeH264Data:ppsData];
}
// 3. 獲取編碼后的數據,寫入文件
// 3.1. 獲取 CMBlockBufferRef
CMBlockBufferRef blcokBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
// 3.2. 從blockBuffer 中獲取起始位置的內存地址
size_t totalLength = 0;
char *dataPointer;
CMBlockBufferGetDataPointer(blcokBuffer, 0, NULL, &totalLength, &dataPointer);
// 3.3. 一幀的圖像可能需要寫入多個NALU 單元 --> Slice切換
static const int H264HeaderLength = 4; // 頭部長度一般為 4
size_t bufferOffset = 0;
while (bufferOffset < totalLength - H264HeaderLength) {
// 3.4 從起始位置拷貝H264HeaderLength長度的地址,計算NALULength
int NALULength = 0;
memcpy(&NALULength, dataPointer + bufferOffset, H264HeaderLength);
// H264 編碼的數據是大端模式(字節序),轉化為iOS系統的模式,計算機內一般都是小端,而網絡和文件中一般都是大端
NALULength = CFSwapInt32BigToHost(NALULength);
// 3.5 從dataPointer開始,根據長度創建NSData
NSData *data = [NSData dataWithBytes:(dataPointer + bufferOffset + H264HeaderLength) length:NALULength];
// 3.6 寫入文件
[encoder writeH264Data:data];
// 3.7 重新設置 bufferOffset
bufferOffset += NALULength + H264HeaderLength;
}
NSLog(@"。。。。。。。。編碼出一幀數據");
};
- (void)writeH264Data:(NSData *)data
{
// 1. 先獲取 startCode
const char bytes[] = "\x00\x00\x00\x01";
// 2. 獲取headerData
// 減一的原因: byts 拼接的是字符串,而字符串最后一位有個 \0;所以減一才是其正確長度
NSData *headerData = [NSData dataWithBytes:bytes length:(sizeof(bytes) - 1)];
[self.fileHandle writeData:headerData];
[self.fileHandle writeData:data];
}
- (void)endEncode
{
VTCompressionSessionInvalidate(self.compressionSession);
CFRelease(_compressionSession);
}
@end
調試步驟
-
下載 VLC 播放器
VLC下載.png
音頻編碼 AAC
- AAC音頻格式有ADIF和ADTS:
- ADIF:Audio Data Interchange Format 音頻數據交換格式。這種格式的特征是可以確定的找到這個音頻數據的開始,ADIF只有一個統一的頭,所以必須得到所有的數據后解碼,即它的解碼必須在明確定義的開始處進行。故這種格式常用在磁盤文件中。
- ADTS:Audio Data Transport Stream 音頻數據傳輸流。這種格式的特征是它是一個有同步字的比特流,解碼可以在這個流中任何位置開始。它的特征類似于mp3數據流格式。語音系統對實時性要求較高,基本流程是采集音頻數據,本地編碼,數據上傳,服務器處理,數據下發,本地解碼,下面學習分析也都以這個格式為例進行。
- AAC原始碼流(又稱為“裸流”)是由一個一個的ADTS frame組成的。其中每個ADTS frame之間通過syncword(同步字)進行分隔。同步字為0xFFF(二進制“111111111111”)。AAC碼流解析的步驟就是首先從碼流中搜索0x0FFF,分離出ADTS frame;然后再分析ADTS frame的首部各個字段。ADTS frame的組成:
- ADTS幀頭包含固定幀頭、可變幀頭,其中定義了音頻采樣率、音頻聲道數、幀長度等關鍵信息,用于幀凈荷數據的解析和解碼。
- ADTS幀凈荷主要由原始幀組成。
- 下圖中表示出了ADTS一幀的簡明結構,其兩邊的空白矩形表示一幀前后的數據。
ADTS 內容及結構
-
ADTS的頭信息,一般都是7個字節 (也有是9字節的),分為2部分:
adts_fixed_header()
和adts_variable_header()
。- ADTS 的固定頭
adts_fixed_header()
結構組成:
adts_fixed_header().jpg- syncword:同步字,12比特的 "1111 1111 1111"
- ID:MPEG 標志符,0表示MPEG-4, 1表示MPEG-2
- layer:表示音頻編碼的層
- protection_absent:表示是否誤碼校驗
- profile:表示使用哪個級別的 AAC
- sampling_frequency_index:表示使用的采樣率索引
- channel_configuration:表示聲道數
- ADTS 的可變頭
adts_variable_header()
結構組成:
adts_variable_header().jpg - aac_frame_lenth:ADTS幀的長度
- adts_buffer_fullness:0x7FF 說明是碼流可變的碼流
- number_of_raw_data_blocks_in_frame:每一個ADTS幀中原始幀的數量
- ADTS 的固定頭
自定義一個
CCAACEncoder
類處理 音頻(裸流) AAC 硬編碼,代碼如下:
//
// CCH264Encoder.h
// 01.視頻采集
//
// Created by zerocc on 2017/4/4.
// Copyright ? 2017年 zerocc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
@interface CCAACEncoder : NSObject
- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer;
- (void)endEncodeAAC;
@end
//
// CCH264Encoder.h
// 01.視頻采集
//
// Created by zerocc on 2017/4/4.
// Copyright ? 2017年 zerocc. All rights reserved.
//
#import "CCAACEncoder.h"
@interface CCAACEncoder()
@property (nonatomic, assign) AudioConverterRef audioConverter;
@property (nonatomic, assign) uint8_t *aacBuffer;
@property (nonatomic, assign) NSUInteger aacBufferSize;
@property (nonatomic, assign) char *pcmBuffer;
@property (nonatomic, assign) size_t pcmBufferSize;
@property (nonatomic, strong) NSFileHandle *audioFileHandle;
@end
@implementation CCAACEncoder
- (void) dealloc {
AudioConverterDispose(_audioConverter);
free(_aacBuffer);
}
- (NSFileHandle *)audioFileHandle {
if (!_audioFileHandle) {
NSString *audioFile = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"audio.aac"];
[[NSFileManager defaultManager] removeItemAtPath:audioFile error:nil];
[[NSFileManager defaultManager] createFileAtPath:audioFile contents:nil attributes:nil];
_audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:audioFile];
}
return _audioFileHandle;
}
- (id)init {
if (self = [super init]) {
_audioConverter = NULL;
_pcmBufferSize = 0;
_pcmBuffer = NULL;
_aacBufferSize = 1024;
_aacBuffer = malloc(_aacBufferSize * sizeof(uint8_t));
memset(_aacBuffer, 0, _aacBufferSize);
}
return self;
}
- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer
{
CFRetain(sampleBuffer);
// 1. 創建audio encode converter
if (!_audioConverter) {
// 1.1 設置編碼參數
AudioStreamBasicDescription inputAudioStreamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer));
AudioStreamBasicDescription outputAudioStreamBasicDescription = {
.mSampleRate = inputAudioStreamBasicDescription.mSampleRate,
.mFormatID = kAudioFormatMPEG4AAC,
.mFormatFlags = kMPEG4Object_AAC_LC,
.mBytesPerPacket = 0,
.mFramesPerPacket = 1024,
.mBytesPerFrame = 0,
.mChannelsPerFrame = 1,
.mBitsPerChannel = 0,
.mReserved = 0
};
static AudioClassDescription description;
UInt32 encoderSpecifier = kAudioFormatMPEG4AAC;
UInt32 size;
AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
sizeof(encoderSpecifier),
&encoderSpecifier,
&size);
unsigned int count = size / sizeof(AudioClassDescription);
AudioClassDescription descriptions[count];
AudioFormatGetProperty(kAudioFormatProperty_Encoders,
sizeof(encoderSpecifier),
&encoderSpecifier,
&size,
descriptions);
for (unsigned int i = 0; i < count; i++) {
if ((kAudioFormatMPEG4AAC == descriptions[i].mSubType) &&
(kAppleSoftwareAudioCodecManufacturer == descriptions[i].mManufacturer)) {
memcpy(&description , &(descriptions[i]), sizeof(description));
}
}
AudioConverterNewSpecific(&inputAudioStreamBasicDescription,
&outputAudioStreamBasicDescription,
1,
&description,
&_audioConverter);
}
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
NSError *error = nil;
if (status != kCMBlockBufferNoErr) {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
memset(_aacBuffer, 0, _aacBufferSize);
AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = 1;
outAudioBufferList.mBuffers[0].mDataByteSize = (int)_aacBufferSize;
outAudioBufferList.mBuffers[0].mData = _aacBuffer;
AudioStreamPacketDescription *outPacketDescription = NULL;
UInt32 ioOutputDataPacketSize = 1;
status = AudioConverterFillComplexBuffer(_audioConverter, inInputDataProc, (__bridge void *)(self), &ioOutputDataPacketSize, &outAudioBufferList, outPacketDescription);
if (status == 0) {
NSData *rawAAC = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData
length:outAudioBufferList.mBuffers[0].mDataByteSize];
NSData *adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
[fullData appendData:rawAAC];
[self.audioFileHandle writeData:fullData];
} else {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
}
/**
編碼器回調 C函數
@param inAudioConverter xx
@param ioNumberDataPackets xx
@param ioData xx
@param outDataPacketDescription xx
@param inUserData xx
@return xx
*/
OSStatus inInputDataProc(AudioConverterRef inAudioConverter,
UInt32 *ioNumberDataPackets,
AudioBufferList *ioData,
AudioStreamPacketDescription **outDataPacketDescription,
void *inUserData)
{
CCAACEncoder *encoder = (__bridge CCAACEncoder *)(inUserData);
UInt32 requestedPackets = *ioNumberDataPackets;
// 填充PCM到緩沖區
size_t copiedSamples = encoder.pcmBufferSize;
ioData->mBuffers[0].mData = encoder.pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (int)encoder.pcmBufferSize;
encoder.pcmBuffer = NULL;
encoder.pcmBufferSize = 0;
if (copiedSamples < requestedPackets) {
//PCM 緩沖區還沒滿
*ioNumberDataPackets = 0;
return -1;
}
*ioNumberDataPackets = 1;
return noErr;
}
/**
* 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.
*/
- (NSData *) adtsDataForPacketLength:(NSUInteger)packetLength {
int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
// Variables Recycled by addADTStoPacket
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //MPEG-4 Audio Channel Configuration. 1 Channel front-center
NSUInteger fullLength = adtsLength + packetLength;
// fill in ADTS data
packet[0] = (char)0xFF; // 11111111 = syncword
packet[1] = (char)0xF9; // 1111 1 00 1 = syncword MPEG-2 Layer CRC
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;
}
- (void)endEncodeAAC
{
AudioConverterDispose(_audioConverter);
_audioConverter = nil;
}
@end
- 測試音視頻
- 采集音視頻
CCVideoCapture
類中導入CCH264Encoder
和CCAACEncoder
- 采集音視頻
#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>
#import "CCH264Encoder.h"
#import "CCAACEncoder.h"
@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@property (nonatomic, strong) CCH264Encoder *videoEncoder;
@property (nonatomic , strong) CCAACEncoder *audioEncoder;
@end
@implementation CCVideoCapture
#pragma mark - lazyload
- (CCH264Encoder *)videoEncoder {
if (!_videoEncoder) {
_videoEncoder = [[CCH264Encoder alloc] init];
}
return _videoEncoder;
}
- (CCAACEncoder *)audioEncoder {
if (!_audioEncoder) {
_audioEncoder = [[CCAACEncoder alloc] init];
}
return _audioEncoder;
}
- (void)startCapturing:(UIView *)preView
{
// =============== 準備編碼 ===========================
[self.videoEncoder prepareEncodeWithWidth:720 height:1280];
// =============== 采集視頻 ===========================
// 1. 創建 session 會話
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPreset1280x720;
self.session = session;
.
.
.
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (self.videoConnection == connection) {
dispatch_sync(queue, ^{
[self.videoEncoder encoderFram:sampleBuffer];
});
NSLog(@"采集到視頻數據");
} else {
dispatch_sync(queue, ^{
[self.audioEncoder encodeAAC:sampleBuffer];
});
NSLog(@"采集到音頻數據");
}
NSLog(@"采集到視頻畫面");
}
- (void)stopCapturing
{
[self.videoEncoder endEncode];
[self.audioEncoder endEncodeAAC];
[self.session stopRunning];
[self.layer removeFromSuperlayer];
}
未完待續
參考資料
編碼和封裝、推流和傳輸
AAC以 ADTS 格式封裝的分析
[音頻介紹](https://wenku.baidu.com/view/278f415e804d2b160b4ec0ac.html%20%20)