由于目前市面上關(guān)于移動端的音視頻開發(fā)書籍極少,因此當(dāng)?shù)弥?lt;<音視頻開發(fā)進(jìn)階指南>>上市后拉鹃,我就立馬買了,然后如饑似渴廢寢忘食的讀了一遍策精。首先揭斧,我覺得這本書寫的很好莱革,循序漸進(jìn)的一步步帶我們從0開始到開發(fā)出一個相對成熟的音視頻應(yīng)用。但是這本書也很"坑"(為什么說"坑"?因為據(jù)我估計讹开,初次看本書的同學(xué)盅视,肯定堅持不下去,因為中間任何一步斷了就會導(dǎo)致看書進(jìn)行不下去)旦万,本文的目的就是填平這些坑闹击,確保能順利的把本書讀完。
本文請配合demo閱讀成艘,demo地址為AudioVideoDemo文件夾赏半。
接下來我會把每章覺得重點的地方都記錄下來贺归。本文是基于iOS端的。
第1章 音視頻基礎(chǔ)概念
通常說的音頻的裸數(shù)據(jù)格式就是脈沖編碼調(diào)制(Pulse Code Modulation, PCM )數(shù)據(jù)断箫。
常見的無損壓縮:WAV編碼拂酣。PCM加上描述信息
常見的有損壓縮:MP3編碼。AAC編碼瑰枫。
YUV和RGB的轉(zhuǎn)化:
典型場景:在iOS平臺中使用攝像頭采集出YUV數(shù)據(jù)后踱葛,上傳顯卡成為一個紋理ID,這個時候就需要做YUV到RGB的轉(zhuǎn)換。
在iOS的攝像頭采集出一幀數(shù)據(jù)之后(CMSampleBufferRef)光坝,我們可以在其中調(diào)用CVBufferGetAttachment來獲取YCbCrMatrix,用于決定使用哪一個矩陣進(jìn)行轉(zhuǎn)換甥材。
常見的視頻編碼:MPEG-4 , H.264
編碼概念: IPB幀盯另。
I幀: 幀內(nèi)編碼幀。I幀壓縮可去掉視頻的空間冗余信息洲赵。
P幀: 前向預(yù)測編碼幀鸳惯,也稱預(yù)測幀。
B幀:雙向預(yù)測內(nèi)插編碼幀叠萍,也稱雙向預(yù)測幀芝发。P幀與B幀是為了去掉時間冗余信息。
PTS和DTS苛谷。 DTS辅鲸,Decoding Time Stamp,解碼時間戳,用于視頻的解碼腹殿,PTS,呈現(xiàn)時間戳独悴,用于解碼階段視頻的同步和輸出。
根據(jù)不同的業(yè)務(wù)場景锣尉,適當(dāng)?shù)卦O(shè)置gop_size刻炒,得到更高質(zhì)量的視頻
第2章 移動端環(huán)境搭建
LAME 是最好的MP3編碼器,編碼高品質(zhì)MP3的最好也是唯一的選擇
FDK_AAC是用來編碼和解碼AAC格式音頻文件的開源庫
X264是一個開源的H.264/MPEG視頻編碼函數(shù)庫自沧,是最好的有損壓縮編碼器之一坟奥。一般輸入是視頻幀的YUV表示,輸出是編碼之后的H264數(shù)據(jù)包拇厢。
項目實踐 : 將PCM文件通過LAME編碼為MP3文件爱谁。
填坑 : 本章作者給的幾個腳本都是有問題的,不能直接交叉編譯出想要的庫旺嬉,所以了解下交叉編譯的概念就好管行,不用太糾結(jié),自己去網(wǎng)上找寫好的交叉編譯腳本(腳本地址)邪媳。我們的目的就是編譯出目的庫捐顷,所以不要被錯誤的腳本卡住了荡陷。
第3章 FFmpeg的介紹與使用
FFmpeg基礎(chǔ)知識:8個靜態(tài)庫,8個模塊
AVUtil: 核心工具庫迅涮,最基礎(chǔ)的模塊
AVFormat : 文件格式和協(xié)議庫废赞,最重要的模塊之一
AVCodec : 編解碼庫,最重要的模塊之一叮姑,但是不會默認(rèn)添加像FDK-AAC等庫唉地,而是像平臺一樣,可以將其他的第三方Codec以插件的方式添加進(jìn)來传透,為開發(fā)者提供統(tǒng)一的接口
AVFilter : 音視頻濾鏡庫耘沼,提供了音視頻特效處理
AVDevice : 輸入輸出設(shè)備庫
SwResample : 用于音頻重采樣,可以對數(shù)字音頻進(jìn)行聲道數(shù)朱盐、數(shù)據(jù)格式群嗤、采樣率等多種基本信息的轉(zhuǎn)換
SWScale : 對圖像進(jìn)行格式的轉(zhuǎn)換,如將YUV的數(shù)據(jù)轉(zhuǎn)換為RGB
下面介紹一些重要的結(jié)構(gòu)體兵琳,
AVFormatContext : 就是對容器或媒體文件的抽象狂秘,包含了多路流(音頻流、視頻流躯肌、字幕流)者春,對流的抽象就是AVStream,在每一路流中都有描述這路流的編碼格式清女,對編解碼格式以及編解碼器的抽象就是AVCodecContext與AVCodec钱烟,對于編碼器或者解碼器的輸入輸出部分,也就是壓縮數(shù)據(jù)以及原始數(shù)據(jù)的抽象就是AVPacket與AVFrame校仑。
FFmpeg中的bit stream filter忠售,用于應(yīng)對某些格式的封裝轉(zhuǎn)換行為。
H264的bit stream filter常常應(yīng)用于視頻解碼過程中迄沫,特別是各個平臺上提供的硬件編碼器時稻扬,一定會用到它。
項目實踐 : 把一個視頻文件解碼成單獨的音頻PCM文件和視頻YUV文件羊瘩。在這個基礎(chǔ)實踐中熟悉FFmpeg的使用步驟泰佳。
FFmpeg的使用步驟(FFmpeg用多了其實不難)
1.引用FFmpeg相應(yīng)模塊的頭文件
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/samplefmt.h"
#include "libavutil/common.h"
#include "libavutil/channel_layout.h"
#include "libavutil/opt.h"
#include "libavutil/imgutils.h"
#include "libavutil/mathematics.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
};
2.注冊協(xié)議、格式與編解碼器
av_register_all();
3.打開媒體文件源尘吗,并設(shè)置超時回調(diào)
AVFormatContext *formatCtx = avformat_alloc_context();
AVIOInterruptCB int_cb = {interrupt_callback, (__bridge void *)(self)};
formatCtx->interrupt_callback = int_cb; //設(shè)置超時回調(diào)
avformat_open_input(&avFormatContext, audioFile, NULL, NULL); //打開媒體文件
avformat_find_stream_info(avFormatContext, NULL); //檢查在文件中的流的信息逝她,其實就是填充avFormatContext
4.尋找各個流,并且打開對應(yīng)的解碼器
坑點:本章的配套demo未處理視頻睬捶,只處理了音頻黔宛,所以第一次看的時候我懵了,跟書本根本對應(yīng)不起來擒贸。
// 本demo并沒有提供視頻文件臀晃,所以只處理了音頻觉渴,未處理視頻(這是跟書里面不同的地方)
AVStream * audioStream = avFormatContext->streams[stream_index];// 找到音頻或者視頻流(注意找序號,有可能是數(shù)組)
formatCtx->streams[i]->codec->codec_type //找出流的類型
avCodecContext = audioStream->codec; //找出解碼格式
AVCodec * avCodec = avcodec_find_decoder(avCodecContext->codec_id); //根據(jù)解碼格式找到對應(yīng)的解碼器
avcodec_open2(avCodecContext, avCodec, NULL);// 打開解碼器
5.初始化解碼后數(shù)據(jù)的結(jié)構(gòu)體
5.1構(gòu)建音頻的格式轉(zhuǎn)換對象以及音頻解碼后數(shù)據(jù)的存放的對象
if (avCodecContext->sample_fmt != AV_SAMPLE_FMT_S16) // 如果不等于AV_SAMPLE_FMT_S16這種可以FFmpeg可以直接使用的格式徽惋,就需要轉(zhuǎn)換案淋。
{
// swr_alloc_set_opts是初始化上下文方法,傳入原始音頻的格式(包括聲道险绘,采樣率踢京,表示格式)和目標(biāo)音頻的格式(包括聲道,采樣率宦棺,表示格式)
// 就可以得到空的重采樣上下文
SwrContext *swrContext = swr_alloc_set_opts(NULL, av_get_default_channel_layout(codecCtx->channels), AV_SAMPLE_FMT_S16, codecCtx->sample_rate, av_get_default_channel_layout(codecCtx->channels), codecCtx->sample_fmt, codecCtx->sample_rate, 0, NULL);
// 創(chuàng)建空的音頻AVFrame(An AVFrame filled with default values)
_audioFrame = avcodec_alloc_frame();
}
5.2構(gòu)建視頻的格式轉(zhuǎn)換對象以及視頻解碼后數(shù)據(jù)的存放的對象
// 如果不等于PIX_FMT_YUV420P這種可以FFmpeg可以直接使用的格式瓣距,就需要轉(zhuǎn)換。
_pictureValid = avpicture_alloc(&_picture,
PIX_FMT_YUV420P,
_videoCodecCtx->width,
_videoCodecCtx->height) == 0;
if (!_pictureValid)
return NO;
// sws_getCachedContext是獲取上下文方法代咸,傳入原始視頻的格式(包括視頻寬旨涝,高,表示格式)和目標(biāo)視頻的格式(包括視頻寬侣背,高,表示格式)
// 就可以得到空的重采樣上下文
_swsContext = sws_getCachedContext(_swsContext,
_videoCodecCtx->width,
_videoCodecCtx->height,
_videoCodecCtx->pix_fmt,
_videoCodecCtx->width,
_videoCodecCtx->height,
PIX_FMT_YUV420P,
SWS_FAST_BILINEAR,
NULL, NULL, NULL);
// 創(chuàng)建空的視頻AVFrame(An AVFrame filled with default values)
_videoFrame = avcodec_alloc_frame();
6.讀取流內(nèi)容并且解碼
打開解碼器后慨默,可以讀取一部分流中的數(shù)據(jù)(AVPacket)贩耐,將它作為解碼器的輸入,解碼器將其解碼為原始數(shù)據(jù)(裸數(shù)據(jù)AVFrame)
AVPacket packet;
BOOL finished = NO;
while (!finished)
{
if (pktStreamIndex ==_videoStreamIndex) //視頻的解碼
{
if (av_read_frame(_formatCtx, &packet) < 0) {
break;
}
avcodec_decode_video2(_videoCodecCtx, _videoFrame,&gotframe,&packet);
if (gotframe)
{
frame = [self handleVideoFrame];//視頻具體處理見下面的7
}
}
else if (pktStreamIndex == _audioStreamIndex)
{
avcodec_decode_audio4(_audioCodecCtx, _audioFrame,&gotframe,&packet);
if (gotframe)
{
AudioFrame * frame = [self handleAudioFrame];//音頻具體處理見下面的7
}
}
7.處理解碼之后的裸數(shù)據(jù)
解碼之后就會得到裸數(shù)據(jù)厦取,音頻就是PCM數(shù)據(jù)潮太,視頻就是YUV數(shù)據(jù),下面將其處理成我們需要的格式并且進(jìn)行寫文件
7.1處理音頻PCM數(shù)據(jù)
- (AudioFrame *) handleAudioFrame
{
void * audioData;
NSInteger numFrames;
if (swrContext) {
const int ratio = 2;
const int bufSize = av_samples_get_buffer_size(NULL,
numChannels, pAudioFrame->nb_samples * ratio,
AV_SAMPLE_FMT_S16, 1);
if (!swrBuffer || swrBufferSize < bufSize) {
swrBufferSize = bufSize;
swrBuffer = realloc(swrBuffer, swrBufferSize);
}
byte *outbuf[2] = { (byte*) swrBuffer, NULL };
// swr_convert 填充AVFrame到swrContext上下文中
numFrames = swr_convert(swrContext, outbuf,
pAudioFrame->nb_samples * ratio,
(const uint8_t **) pAudioFrame->data,
pAudioFrame->nb_samples);
audioData = swrBuffer;
} else {
audioData = pAudioFrame->data[0];
numFrames = pAudioFrame->nb_samples;
}
}
7.2 處理視頻YUV數(shù)據(jù)
- (VideoFrame *) handleVideoFrame
{
VideoFrame *frame = [[VideoFrame alloc] init];
if(_videoCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P || _videoCodecCtx->pix_fmt == AV_PIX_FMT_YUVJ420P){
frame.luma = copyFrameData(_videoFrame->data[0],
_videoFrame->linesize[0],
_videoCodecCtx->width,
_videoCodecCtx->height);
frame.chromaB = copyFrameData(_videoFrame->data[1],
_videoFrame->linesize[1],
_videoCodecCtx->width / 2,
_videoCodecCtx->height / 2);
frame.chromaR = copyFrameData(_videoFrame->data[2],
_videoFrame->linesize[2],
_videoCodecCtx->width / 2,
_videoCodecCtx->height / 2);
}
else
{
// 處理圖像數(shù)據(jù)虾攻。
// sws_scale將輸出的_videoFrame(AVFrame類型)轉(zhuǎn)換成為AVPicture铡买,再從AVPicture提取對應(yīng)的數(shù)據(jù)封裝到自定義的結(jié)構(gòu)體(VideoFrame)中
sws_scale(_swsContext,
(const uint8_t **)_videoFrame->data,
_videoFrame->linesize,
0,
_videoCodecCtx->height,
_picture.data,
_picture.linesize);
frame.luma = copyFrameData(_picture.data[0],
_picture.linesize[0],
_videoCodecCtx->width,
_videoCodecCtx->height);
frame.chromaB = copyFrameData(_picture.data[1],
_picture.linesize[1],
_videoCodecCtx->width / 2,
_videoCodecCtx->height / 2);
frame.chromaR = copyFrameData(_picture.data[2],
_picture.linesize[2],
_videoCodecCtx->width / 2,
_videoCodecCtx->height / 2);
}
}
8.關(guān)閉所有資源
代碼略
有可能還是看不懂這章的代碼,沒關(guān)系霎箍,到了第5章你就懂了(坑點:很多內(nèi)容必須要等到看到后面幾章才明白)
第4章 移動平臺下音視頻渲染
iOS平臺下利用AudioUnit來渲染音頻
項目實踐 : 使用AudioUnit完成一個音頻播放的功能的步驟:創(chuàng)建音頻會話奇钞、構(gòu)建一個AudioUnit,并給AudioUnit設(shè)置參數(shù)(再介紹AudioUnit的分類),最后構(gòu)建一個AUGraph完成音頻播放
1.創(chuàng)建音頻會話漂坏,并設(shè)置合適的參數(shù)(使用AVAudioSession 代碼略)
2.構(gòu)建AudioUnit
(1)構(gòu)造AUGraph
NewAUGraph(&mPlayerGraph);
(2).構(gòu)建AudioComponentDescription
AudioComponentDescription ioDescription;
ioDescription.componentType = kAudioUnitType_Output; // 后面詳細(xì)介紹
ioDescription.componentSubType = kAudioUnitSubType_RemoteIO; // 與上面對應(yīng)
ioDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
(3).添加Node
AUGraphAddNode(mPlayerGraph, &ioDescription, &mPlayerIONode);
(4).打開Graph, 只有真正的打開了Graph才會實例化每一個Node
AUGraphOpen(mPlayerGraph);
(5)獲取出某個node的AudioUnit
AUGraphNodeInfo(mPlayerGraph, mPlayerIONode, NULL, &mPlayerIOUnit);
3.AudioUnit的通用參數(shù)設(shè)置
// 配置AudioStreamBasicDescription
AudioStreamBasicDescription asbd;
bzero(&asbd, sizeof(asbd));
asbd.mSampleRate = _sampleRate;
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
asbd.mBitsPerChannel = 8 * bytesPerSample;
asbd.mBytesPerFrame = bytesPerSample;
asbd.mBytesPerPacket = bytesPerSample;
asbd.mFramesPerPacket = 1;
asbd.mChannelsPerFrame = channels;
使用AudioStreamBasicDescription給AudioUnit設(shè)置屬性
AudioUnitSetProperty(_ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, inputElement,
&streamFormat, sizeof(streamFormat));
4.連接起Node
方式一:直接連接
AUGraphConnectNodeInput(_auGraph, _convertNode, 0, _ioNode, 0);
方式二:回調(diào)
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &InputRenderCallback;
callbackStruct.inputProcRefCon = (__bridge void *)self;
5.最后播放
CAShow(_auGraph);
AUGraphInitialize(_auGraph);
AUGraphStart(_auGraph);
安卓和iOS都利用OpenGL ES來渲染視頻
注意: 因為OpenGL不負(fù)責(zé)窗口管理及上下文環(huán)境管理景埃,所以需要各平臺(如iOS平臺)自己提供OpenGL ES的上下文環(huán)境和窗口管理
OpenGL ES實踐
基礎(chǔ)知識
Vertex Shader(頂點著色器)替換頂點處理階段 attribute,用于經(jīng)常更改的信息,只能用于頂點著色器 (varying,用于修飾從頂點往片元傳遞的變量)
gl_Position
Fragment Shader(片元著色器顶别,又稱像素著色器)替換片元處理階段 uniform,用于不經(jīng)常更改的信息谷徙,可以用于2種著色器
gl_FragColor
窗口創(chuàng)建的步驟
1.創(chuàng)建窗口
glCreateProgram();
2.創(chuàng)建Vertex Shader和Fragment Shader
(1)創(chuàng)建shader
GLuint shader = glCreateShader(type);
(2)加載資源
glShaderSource(shader, 1, &sources, NULL);
(3)編譯
glCompileShader(shader);
3.關(guān)聯(lián)著色器到窗口
glAttachShader(filterProgram, vertShader);
4.鏈接
glLinkProgram(filterProgram);
5.使用窗口
glUseProgram(filterProgram);
上下文環(huán)境搭建
1.編寫View類繼承UIView,重寫layerClass,返回CAEAGLLayer
+ (Class) layerClass
{
return [CAEAGLLayer class];
}
2.然后在initWithFrame中獲取layer并且強(qiáng)制轉(zhuǎn)為CAEAGLLayer,并設(shè)置色彩模式等參數(shù)
CAEAGLLayer *eaglLayer = (CAEAGLLayer*) self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
nil];
3.在線程中構(gòu)造上下文驯绎,并且綁定
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:_context];
4.創(chuàng)建幀緩沖區(qū)
glGenFramebuffers(1, &_displayFramebuffer);
創(chuàng)建繪制緩沖區(qū)
glGenRenderbuffers(1, &_renderbuffer);
綁定幀緩沖區(qū)到渲染管線
glBindFramebuffer(GL_FRAMEBUFFER, _displayFramebuffer);
綁定繪制緩沖區(qū)到渲染管線
glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer);
為繪制緩沖區(qū)分配存儲區(qū)完慧,此處將CAEAGLLayer的繪制存儲區(qū)作為繪制緩沖區(qū)的存儲區(qū)
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
獲取繪制緩沖區(qū)的像素寬度
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
獲取繪制緩沖區(qū)的像素高度
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
將繪制緩沖區(qū)綁定到幀緩沖區(qū)
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderbuffer);
呈現(xiàn)renderBuffer,完成
[_context presentRenderbuffer:GL_RENDERBUFFER];
繪制的過程
1)規(guī)定窗口的大小
glViewport(0, _backingHeight - _backingWidth - 75, _backingWidth, _backingWidth);
2)使用顯卡繪制程序
glUseProgram(filterProgram);
3)設(shè)置物體坐標(biāo)
GLfloat imageVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, imageVertices);
glEnableVertexAttribArray(filterPositionAttribute);
4)設(shè)置紋理坐標(biāo)
GLfloat noRotationTextureCoordinates[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, noRotationTextureCoordinates);
glEnableVertexAttribArray(filterTextureCoordinateAttribute);
5)指定將要繪制的紋理對象并且傳遞給對應(yīng)的Fragment Shader
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _inputTexture);
glUniform1i(filterInputTextureUniform, 0);
6)執(zhí)行繪制操作
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
7)最后刪除紋理對象
glDeleteTextures(1, &_inputTexture);
小tip : GPUImage是一個開源的圖像處理工具,它是基于OpenGL ES實現(xiàn)的剩失。
第5章 實現(xiàn)一款視頻播放器
架構(gòu)設(shè)計: 解碼模塊屈尼,音頻播放模塊册着,視頻播放模塊,音視頻同步鸿染,中控系統(tǒng)指蚜。
5.1 架構(gòu)設(shè)計
輸入:本地磁盤的一個媒體文件(可能flv、mp4涨椒、avi摊鸡、mov等格式)或者網(wǎng)絡(luò)上的媒體文件(可能是http、rtmp蚕冬、hls等協(xié)議)
輸出:視頻中的音頻放到揚(yáng)聲器讓人聽到免猾,視頻畫面渲染到屏幕讓人看到,同時確保聲音畫面同步囤热。
輸入模塊:由媒體文件解析得到音頻流和視頻流猎提,然后將這兩路流都解碼為裸數(shù)據(jù),然后為音視頻各建立一個隊列將裸數(shù)據(jù)存儲起來旁蔼。
音頻輸出和視頻輸出模塊:去音頻視頻隊列中拿出裸數(shù)據(jù)锨苏,然后分別進(jìn)行音視頻的渲染。
音視頻同步模塊:負(fù)責(zé)音視頻同步
調(diào)度器模塊:負(fù)責(zé)將以上模塊組裝起來
具體模塊說明:
AudioFrame:音頻幀棺聊,這里面記錄了音頻的數(shù)據(jù)格式以及這一幀的具體數(shù)據(jù)伞租、時間戳等信息;
VideoFrame:視頻幀限佩,記錄了視頻的格式以及這一幀的具體的數(shù)據(jù)葵诈、寬、高以及時間戳等信息祟同;
AudioFrameQueue:音頻隊列作喘,主要用于存儲音頻幀,為它的客戶端代碼音視頻同步模塊提供壓入和彈出操作晕城,由于解碼線程和聲音播放線程會作為生產(chǎn)者和消費(fèi)者同時訪問這個隊列中的元素泞坦,所以這個隊列要保證線程安全性;(本項目中是一個可變數(shù)組)
VideoFrameQueue:視頻隊列广辰,主要用于存儲視頻幀暇矫,為它的客戶端代碼音視頻同步模塊提供壓入和彈出操作,由于解碼線程和視頻播放線程會作為生產(chǎn)者和消費(fèi)者同時訪問這個隊列中的元素择吊,所以這個隊列保證線程安全性李根;(本項目中是一個可變數(shù)組)
VideoDecoder:輸入模塊,職責(zé)在前面已經(jīng)分析了几睛,由于還沒有確定具體的技術(shù)實現(xiàn)房轿,所以這里根據(jù)前面的分析寫了三個實例變量,一個是協(xié)議層解析器,一個是格式解封裝器囱持,一個是解碼器夯接,并且它主要向AVSynchronizer暴露接口:打開文件資源(網(wǎng)絡(luò)或者本地)、關(guān)閉文件資源纷妆、解碼出一定時間長度的音視頻幀盔几。
AudioOutput:音頻輸出模塊,由于在不同平臺有不同的實現(xiàn)掩幢,所以這里真正的聲音渲染API為Void類型逊拍,但是音頻的渲染要放在單獨的一個線程(不論是平臺API自動提供的線程,還是我們主動建立的線程)中進(jìn)行际邻,所以這里有一個線程的變量芯丧,在運(yùn)行過程中會調(diào)用注冊過來的回調(diào)函數(shù)來獲取音頻數(shù)據(jù);
VideoOutput:視頻輸出模塊世曾,雖然這里統(tǒng)一使用OpenGL ES來渲染視頻缨恒,但是前面也講過,OpenGL ES的具體實現(xiàn)在不同平臺也會有自己的上下文環(huán)境轮听,所以這里采用了Void類型的實現(xiàn)骗露,當(dāng)然,必須由我們主動開啟一個線程來作為OpenGL ES的渲染線程血巍,它會在運(yùn)行過程中調(diào)用注冊過來的回調(diào)函數(shù)獲取視頻數(shù)據(jù)椒袍;
AVSynchronizer:音視頻同步模塊,組合輸入模塊及音頻隊列和視頻隊列藻茂,這里面主要對它的客戶端代碼VideoPlayerController這個調(diào)度器提供接口,包括:開始玫恳、結(jié)束辨赐,以及最重要的獲取音頻數(shù)據(jù)和獲取對應(yīng)時間戳的視頻幀。此外京办,它也會維護(hù)一個解碼線程掀序,并且根據(jù)音視頻隊列里面的元素數(shù)目來繼續(xù)或者暫停這個解碼線程的運(yùn)行;
VideoPlayerController:調(diào)度器惭婿,內(nèi)部維護(hù)音視頻同步模塊不恭、音頻輸出模塊、視頻輸出模塊财饥,向客戶端代碼暴露接口:開始播放换吧、暫停、繼續(xù)播放钥星、停止播放接口沾瓦;向音頻輸出模塊和視頻輸出模塊暴露兩個獲取數(shù)據(jù)的接口;
第6章 音視頻的采集與編碼
6.1 音頻的采集
iOS端使用AudioUnit采集音頻
6.2 視頻畫面的采集
ELImageProgram : 把OpenGL的Program的構(gòu)建、查找屬性贯莺、使用等操作以面向?qū)ο蟮男问椒庋b起來的類风喇。
ELImageTextureFrame:用于將紋理對象和幀緩存對象的創(chuàng)建、綁定缕探、銷毀等操作以面向?qū)ο蟮姆绞椒庋b起來的類魂莫。
ELImageContext:OpenGL ES渲染操作必須執(zhí)行在綁定了OpenGL上下文的線程中,而且由于其對于客戶端代碼的調(diào)用爹耗,需要在調(diào)用線程和OpenGL ES的線程之間進(jìn)行頻繁的切換耙考,所以提供一個靜態(tài)方法可以獲得具有OpenGL上下文的渲染線程,讓渲染操作可以直接在該線程中執(zhí)行鲸沮。
ELImageInput:協(xié)議琳骡。方法一:設(shè)置輸入紋理對象。方法二:進(jìn)行渲染操作讼溺。
ELImageOutput:需要向后級節(jié)點輸出紋理對象的節(jié)點的類楣号。
6.3 音頻的編碼
6.3.1 libfdk_aac編碼AAC
使用libfdk_aac軟編碼將PCM文件編碼為AAC文件