音視頻學習總結

從零開始做一個小播放器

—音視頻學習總結


1.視頻播放


1.1視頻文件格式與編碼格式


1.1.1文件格式


常見的視頻文件格式MPG、TS、AVI、RMVB、AVI等等

他們分別是以特定的方式將音頻、圖像數據按順序編碼在一起,形成視頻文件。


以AVI(Audio Video Interleaved)格式的視頻爲例,說明下關係。AVI採用的是RIFF文件結構方式。構造RIFF文件的基本單元叫做數據塊(chunk),每個數據塊包含三個部分:


1)數據塊的ID

2)  數據塊的大小

3)數據


整個RIFF文件可以看成一個數據塊,其數據塊ID爲RIFF。一個RIFF文件中只允許存在一個RIFF塊。RIFF塊中包含一系列的子塊,其中有一種字塊的ID爲"LIST",稱爲LIST,LIST塊中可以再包含一系列的子塊,但除了LIST塊外的其他所有的子塊都不能再包含子塊。RIFF和LIST塊分別比普通的數據塊多一個被稱爲形式類型(Form Type)和列表類型(List Type)的數據域,其組成如下: 


1)4字節的數據塊標記(Chunk ID)

2)數據塊的大小

3)4字節的形式類型或者列表類型

4)數據


AVI的RIFF塊的形式類型是AVI,它包含3個子塊,如下所述:


1)信息塊,一個ID爲”hdrl"的LIST塊,定義AVI文件的數據格式。

2)數據塊,一個ID爲 "movi"的LIST塊,包含AVI的音視頻序列數據。

3)索引塊,ID爲 "idxl"的子塊,定義 “movi”LIST塊的索引數據,是可選塊。


其中數據塊的音視頻是交叉排列的,一段video後面接一段audio,然後再接video,如此循環下去


1.1.2編碼格式


編碼格式指的的video數據和audio數據按照一定的方式進行編碼。


常見的video編碼有MPG1、MPG2、MPG4、H264、H263等等

常見的audio編碼格式有AAC、MP3、WMA等等


視頻中的video數據塊包含若干幀圖像,這些圖像是按照video的編碼方式存儲在裏面,將圖像依次在屏幕上播放,利用人眼的視覺暫留特性,就形成了我們看到的視頻。


H264的分層結構:VCL   video coding layer    視頻編碼層;NAL   network abstraction layer   網絡提取層; 



網絡傳輸的H264是由許多的NALU組成,每個NALU又包含NAL頭和RBSP。NALU頭用來標識後面的RBSP是什麼類型的數據,它是否會被其他幀參考以及網絡傳輸是否有錯誤。RBSP是NAL傳輸的基本單元,包括序列參數集 SPS 和 圖像參數集 PPS等。SPS 包含的是針對一連續編碼視頻序列的參數,如標識符 seq_parameter_set_id、幀數及 POC 的約束、參考幀數目、解碼圖像尺寸和幀場編碼模式選擇標識等等。PPS對應的是一個序列中某一幅圖像或者某幾幅圖像,其參數如標識符 pic_parameter_set_id、可選的 seq_parameter_set_id、熵編碼模式選擇標識、片組數目、初始量化參數和去方塊濾波係數調整標識等等。


H264中把圖像分成一幀(frame)或兩場(field)(來源於老式的那種隔行掃描電視的概念,奇數行算一場,偶數的算另一場,由於數據傳輸的問題,所以分爲兩場來傳輸新型號,這樣由於人眼有信息暫留,所以不影響最後的效果),而幀又可以分成一個或幾個片(Slilce);片由宏塊(MB)組成。宏塊是編碼處理的基本單元,一個宏塊由一個16×16亮度像素和附加的一個8×8 Cb和一個8×8 Cr彩色像素塊組成。幀又分爲I幀、P幀和B幀,分別爲關鍵幀,預測幀和中間幀(依賴I幀、P幀或其他B幀的數據)


由此將NAL中相同的slice組成一幀圖像,然後連續的I幀、P幀、B幀組合起來播放,形成了完整的video。


參考:http://blog.csdn.net/mincheat/article/details/48713047


1.2.1圖像格式、數據結構


由1.2.1介紹,圖像是video的基礎。圖像是由N多像素點組成的。像素點的色彩表示方法有兩種:RBG和YUV。RBG類型的像素點,由計算機使用3個字節來分別表示一個像素裏面的Red,Green和Blue的發光強度數值。YUV(YCbCr)是將色彩和亮度分離,用Y表示亮度,U和V來表示色彩信息,這樣做的好處是兼容黑白電視。除了這一點外,同一圖像,用RGB和YUV來表示,YUV的數據量更少。


YUV採樣格式:主要的採樣格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和 YCbCr 4:4:4。其中YCbCr 4:1:1 比較常用,其含義爲:每個點保存一個 8bit 的亮度值(也就是Y值), 每 2 x 2 個點保存一個 Cr和Cb值, 圖像在肉眼中的感覺不會起太大的變化。所以, 原來用 RGB(R,G,B 都是 8bit unsigned) 模型, 每個點需要 8x3=24 bits, 而現在僅需要 8+(8/4)+(8/4)=12bits, 平均每個點佔12bits。這樣就把圖像的數據壓縮了一半。 


關於YUV和RGB的轉換,按照兩者之間的相互關係進行計算轉換。


       Y = 0.299 R + 0.587 G + 0.114 B

       U = -0.1687 R - 0.3313 G + 0.5 B + 128

       V = 0.5 R - 0.4187 G - 0.0813 B + 128


       R = Y + 1.402 (V-128)

       G= Y - 0.34414 (U-128) - 0.71414 (V-128)

      B= Y + 1.772 (U-128)


不同設備所採用的參數略有不同,以上僅供參考。


1.2.2video解碼、渲染


在iOS設備上顯示圖像一般有兩種途徑:將數據轉換成UIIamge,通過UIImageView在屏幕上顯示;圖像數據通過OpenGL ES直接在屏幕上渲染。


前者將RGB格式的數據通過iOS 自帶CoreGraphics framework可以直接轉換成UIIamge投影到屏幕上。但是有個弊病,iOS所帶的顏色空間(color space)只支持RGB的顏色空間,不支持YUV。

YUV格式的數據在iOS設備上需要通過OpenGL ES來顯示,在渲染過程中,可以使用shader提高性能。shader利用GPU進行相關的計算工作,減輕了CPU的負載。


1.2.3FFmpeg之videostream


FFmpeg是一套可以用來記錄、轉換數字音頻、視頻,並能將其轉化爲流的開源庫。主要結構體包括:AVFrame、AVFormatContext、AVCodecContext、AVIOContext、AVCodec、AVStream、AVPacket。


AVFrame:

AVFrame結構體一般用於存儲原始數據(即非壓縮數據,例如對視頻來說是YUV,RGB,對音頻來說是PCM),此外還包含了一些相關的信息:

uint8_t *data[AV_NUM_DATA_POINTERS]:解碼後原始數據(對視頻來說是YUV,RGB,對音頻來說是PCM)

int linesize[AV_NUM_DATA_POINTERS]:data中“一行”數據的大小。注意:未必等於圖像的寬,一般大於圖像的寬。

int width, height:視頻幀寬和高(1920x1080,1280x720...)

int nb_samples:音頻的一個AVFrame中可能包含多個音頻幀,在此標記包含了幾個

int format:解碼後原始數據類型(YUV420,YUV422,RGB24...)

int key_frame:是否是關鍵幀

enum AVPictureType pict_type:幀類型(I,B,P...)

AVRational sample_aspect_ratio:寬高比(16:9,4:3...)

int64_t pts:顯示時間戳

int coded_picture_number:編碼幀序號

int display_picture_number:顯示幀序號

int8_t *qscale_table:QP表

uint8_t *mbskip_table:跳過宏塊表

int16_t (*motion_val[2])[2]:運動矢量表

uint32_t *mb_type:宏塊類型表

short *dct_coeff:DCT係數,這個沒有提取過

int8_t *ref_index[2]:運動估計參考幀列表(貌似H.264這種比較新的標準纔會涉及到多參考幀)

int interlaced_frame:是否是隔行掃描

uint8_t motion_subsample_log2:一個宏塊中的運動矢量採樣個數,取log的


AVFormatContext的幾個主要作用變量:

struct AVInputFormat *iformat:輸入數據的封裝格式

AVIOContext *pb:輸入數據的緩存

unsigned int nb_streams:視音頻流的個數

AVStream **streams:視音頻流

char filename[1024]:文件名

int64_t duration:時長(單位:微秒us,轉換爲秒需要除以1000000)

int bit_rate:比特率(單位bps,轉換爲kbps需要除以1000)

AVDictionary *metadata:元數據


AVCodecContext:

enum AVMediaType codec_type:編解碼器的類型(視頻,音頻...)

struct AVCodec  *codec:採用的解碼器AVCodec(H.264,MPEG2...)

int bit_rate:平均比特率

uint8_t *extradata; int extradata_size:針對特定編碼器包含的附加信息(例如對於H.264解碼器來說,存儲SPS,PPS等)

AVRational time_base:根據該參數,可以把PTS轉化爲實際的時間(單位爲秒s)

int width, height:如果是視頻的話,代表寬和高

int refs:運動估計參考幀的個數(H.264的話會有多幀,MPEG2這類的一般就沒有了)

int sample_rate:採樣率(音頻)

int channels:聲道數(音頻)

enum AVSampleFormat sample_fmt:採樣格式

int profile:型(H.264裏面就有,其他編碼標準應該也有)

int level:級(和profile差不太多)


AVIOContext中有以下幾個變量比較重要:

unsigned char *buffer:緩存開始位置

int buffer_size:緩存大小(默認32768)

unsigned char *buf_ptr:當前指針讀取到的位置

unsigned char *buf_end:緩存結束的位置

void *opaque:URLContext結構體

URLContext結構體中還有一個結構體URLProtocol。每種協議(rtp,rtmp,file等)對應一個URLProtocol,在av_regitser_all()函數中會註冊所支持的URLProtocol。每種URLProtocol結構體大都包含:open、read、seek、close等通用的函數,其他的根據協議的不同,添加各自不同的功能


AVCodec最主要的幾個變量:

const char *name:編解碼器的名字,比較短

const char *long_name:編解碼器的名字,全稱,比較長

enum AVMediaType type:指明瞭類型,是視頻,音頻,還是字幕

enum AVCodecID id:ID,不重複

const AVRational *supported_framerates:支持的幀率(僅視頻)

const enum AVPixelFormat *pix_fmts:支持的像素格式(僅視頻)

const int *supported_samplerates:支持的採樣率(僅音頻)

const enum AVSampleFormat *sample_fmts:支持的採樣格式(僅音頻)

const uint64_t *channel_layouts:支持的聲道數(僅音頻)

int priv_data_size:私有數據的大小


AVStream重要的變量:

int index:標識該視頻/音頻流

AVCodecContext *codec:指向該視頻/音頻流的AVCodecContext(它們是一一對應的關係)

AVRational time_base:時基。通過該值可以把PTS,DTS轉化爲真正的時間。FFMPEG其他結構體中也有這個字段,但是根據我的經驗,只有AVStream中的time_base是可用的。PTS*time_base=真正的時間

int64_t duration:該視頻/音頻流長度

AVDictionary *metadata:元數據信息

AVRational avg_frame_rate:幀率(注:對視頻來說,這個挺重要的)

AVPacket attached_pic:附帶的圖片。比如說一些MP3,AAC音頻文件附帶的專輯封面。


AVPacket結構體中,重要的變量有:

uint8_t *data:壓縮編碼的數據。

(例如對於H.264來說。1個AVPacket的data通常對應一個NAL。在這裏只是對應,而不是一模一樣。他們之間有微小的差別,因此在使用FFMPEG進行視音頻處理的時候,常常可以將得到的AVPacket的data數據直接寫成文件,從而得到視音頻的碼流文件)

int   size:data的大小

int64_t pts:顯示時間戳

int64_t dts:解碼時間戳

int   stream_index:標識該AVPacket所屬的視頻/音頻流。


ffmpeg解碼的流程圖: 



1.3.1音頻格式、數據結構



音頻常見的編碼方式有PCM、MP3和WMA等。


關於採樣率:

聲音其實是一種能量波,因此也有頻率和振幅的特徵,頻率對應於時間軸線,振幅對應於電平軸線。波是無限光滑的,絃線可以看成由無數點組成,由於存儲空間是相對有限的,數字編碼過程中,必須對絃線的點進行採樣。採樣的過程就是抽取某點的頻率值,爲了復原波形,一次振動中,必須有2個點的採樣,人耳能夠感覺到的最高頻率爲20kHz,因此要滿足人耳的聽覺要求,則需要至少每秒進行40k次採樣,用40kHz表達,這個40kHz就是採樣率。

有損無損:

根據採樣率可知,音質只能無限接近原始信號,相對於原始信號,任何音頻編碼都是有損的。通常約定能達到最高水平的保真編碼爲PCM,PCM也只是無限接近。MP3爲有損格式,是常見的音頻壓縮格式



1.3.2音頻播放


iOS中音頻播放的主要方式有兩種:

1.使用AVAudioPlayer播放音頻

2.使用Audio Unit(AU)或Audio Queue(AQ)播放解碼後的音頻

第一種系統已經集成好,直接調用上層API就可以完成了;

第二種是採用底層的音頻API來完成播放,AU和AQ兩種略有不同


AU步驟如下:


1)AudioSessionInitialize初始化一個iOS應用的音頻會話對象

2)配置Audio Session

配置屬性

kAudioSessionCategory_MediaPlayback指定爲音頻播放

kAudioSessionProperty_PreferredHardwareIOBufferDuration配置更小的I/O遲延,通常情況不需要設置 。

配置屬性變化監聽器(觀察者模式的應用),非最小功能要求,可不實現。

kAudioSessionProperty_AudioRouteChange

kAudioSessionProperty_CurrentHardwareOutputVolume

AudioSessionSetActive激活音頻會話

3)配置Audio Unit

描述輸出單元AudioComponentDescription

獲取組件AudioComponent

覈對輸出流格式AudioStreamBasicDescription

設置音頻渲染回調結構體AURenderCallbackStruct並指定回調函數,這是真正向音頻設備提供PCM數據的地方

4)音頻渲染回調函數傳入未播放的音頻數據

5)釋放資源

6)FFmpeg解碼流程

7)音頻重採樣


注:關於“重採樣”,根據雷霄驊的博客,FFmpeg 3.0 avcodec_decode_audio4函數解碼出來的音頻數據是單精度浮點類型,值範圍爲[0, 1.0]。iOS可播放Float類型的音頻數據,範圍和FFmpeg解碼出來的PCM不同,故需要進行重採樣。


AQ步驟如下:


AudioQueue的其工作模式,在其內部有一套緩衝隊列(Buffer Queue)的機制。在AudioQueue啓動之後需要通過AudioQueueAllocateBuffer生成若干個AudioQueueBufferRef結構,這些Buffer將用來存儲即將要播放的音頻數據,並且這些Buffer是受生成他們的AudioQueue實例管理的,內存空間也已經被分配(按照Allocate方法的參數),當AudioQueue被Dispose時這些Buffer也會隨之被銷燬。

當有音頻數據需要被播放時首先需要被memcpy到AudioQueueBufferRef的mAudioData中(mAudioData所指向的內存已經被分配,之前AudioQueueAllocateBuffer所做的工作),並給mAudioDataByteSize字段賦值傳入的數據大小。完成之後需要調用AudioQueueEnqueueBuffer把存有音頻數據的Buffer插入到AudioQueue內置的Buffer隊列中。在Buffer隊列中有buffer存在的情況下調用AudioQueueStart,此時AudioQueue就回按照Enqueue順序逐個使用Buffer隊列中的buffer進行播放,每當一個Buffer使用完畢之後就會從Buffer隊列中被移除並且在使用者指定的RunLoop上觸發一個回調來告訴使用者,某個AudioQueueBufferRef對象已經使用完成,你可以繼續重用這個對象來存儲後面的音頻數據。如此循環往復音頻數據就會被逐個播放直到結束。

流程大致如下:

1)創建AudioQueue,創建一個自己的buffer數組BufferArray;

2)使用AudioQueueAllocateBuffer創建若干個AudioQueueBufferRef(一般2-3個即可),放入BufferArray;

3)有數據時從BufferArray取出一個buffer,memcpy數據後用AudioQueueEnqueueBuffer方法把buffer插入AudioQueue中;

4)AudioQueue中存在Buffer後,調用AudioQueueStart播放。(具體等到填入多少buffer後再播放可以自己控制,只要能保證播放不間斷即可);

5)AudioQueue播放音樂後消耗了某個buffer,在另一個線程回調並送出該buffer,把buffer放回BufferArray供下一次使用;

6)返回步驟3)繼續循環直到播放結束



1.4.1網絡視頻播放


Apple有一套自帶的直播框架HLS,動態碼流自適應技術,包含一個索引m3u8和若干個TS文件。主要實現方法爲,將視頻切爲若干個ts片段,根據TS和視頻信息,生成對應的m3u8文件。在播放時,播放器首先獲取到m3u8文件,根據m3u8的內容依次獲取ts文件,下載下來進行播放。


HLS可以用於直播和點播,直播是採集端將採集好的數據切片,製成ts和m3u8文件,存在服務器端,客戶端去不斷請求數據,這種直播方式延遲相對比較高。點播是服務器端已經保存有完整的視頻ts和m3u8數據,客戶端直接請求數據。二者的不同之處在於,直播是m3u8索引文件會不斷更新,而點播獲取的m3u8文件在末尾會有end標識符,不會更新。


其他的網絡播放方式有rtmp、httpflv等


rtmp需要將實時獲取的音視頻分別使用AAC以及AVC的標準進行編碼,將編碼後的數據封裝成flv數據流。需要發送AVC sequence header以及AAC sequence header。對於AVC的一幀的數據,是由多個NALU組成的,需要將這些NALU分開。在傳輸的第一幀之前,需要發送AVC sequence header,而AVC sequence header裏面包含了sps和pps信息,sps以及pps是在類型分別爲sps和pps的NALU中獲取的,這樣的NALU在每個關鍵幀中都會出現。音頻按照類似的處理方式,得到AAC sequence header。最終,可以得到AVC sequence header、AAC sequence header、Data(Video)、Data(Audio),按照協議標準進行傳輸。


ffmpeg自帶rtmp協議支持,可以根據直播鏈接初始化,然後解碼獲取的數據,在opengl或者imageview上播放顯示


1.4.2本地視頻播放


1)iOS自帶avplayer播放器

2)根據上文的ffmpeg流程圖,讀取文件,解碼數據,然後分別對應音頻播放和視頻播放。


2.1音視頻播放線程


關於pthread線程、線程鎖、信號量的使用

音頻、視頻各需要兩個線程,一個解碼線程、一個播放線程。關於線程方面具體是閱讀ijkplayer的源碼來理解這幾個線程的工作機制。以及博客上的參考資料http://blog.csdn.net/hudashi/article/details/7709413


2.2PTS、DTS


DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顧名思義,前者是解碼的時間,後者是顯示的時間。

FFmpeg中用AVPacket結構體來描述解碼前或編碼後的壓縮包,用AVFrame結構體來描述解碼後或編碼前的信號幀。 對於視頻來說,AVFrame就是視頻的一幀圖像。這幀圖像什麼時候顯示給用戶,就取決於它的PTS。DTS是AVPacket裏的一個成員,表示這個壓縮包應該什麼時候被解碼。 如果視頻裏各幀的編碼是按輸入順序(也就是顯示順序)依次進行的,那麼解碼和顯示時間應該是一致的。事實上,在大多數編解碼標準(如H.264或HEVC)中,編碼順序和輸入順序並不一致。 於是纔會需要PTS和DTS這兩種不同的時間戳。


2.3音視頻同步


由於音頻數據和視頻數據是單獨解析單獨播放的,所以存在音視頻播放同步問題。

根據kun的課程,音視頻同步分爲三類,以系統時間爲基準、以視頻時間爲基準、以音頻時間爲基準。因爲人對聲音比對圖像的敏感度要大,所以一般常採用音頻爲基準。其中兩個關鍵參數DTS和PTS,根據解碼音視頻得到的這兩個參數來調整音視頻的播放和解碼關係。同時設置一個δ,當δ大於誤差允許值時,根據情況,適當地通過延遲和提前視頻,以使視頻跟上音頻的節奏。由於音視頻同步比較複雜,在實際播放demo中還沒具體涉及到這一塊,有待後續研究。


2.4視頻緩存


目前iOS上,對於支持rtmp的直播,可以在播放器和服務器之間添加一個代理,代理相當於中轉的作用,播放器向代理髮送datarequest,代理根據request向服務器請求數據,並將獲取到的數據copy成兩份,一份寫入本地文件,一份返回給播放器play。主要利用的是- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest函數。

對於HLS的直播,由於該函數不支持此類協議,所以目前沒有合適的緩存方法。有一種不完善的方法,利用開源的服務器代碼,在iOS本地搭建一個服務器,播放器向本地服務請求數據,本地服務器請求m3u8文件並下載ts,然後返回給播放器,同時儲存數據,但是在兩個ts之間切換時會有卡頓的現象。









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章