iOS VideoToolbox硬解H.264數據說明

一. 概述

蘋果從iOS 8開始,開放了硬編碼和硬解碼的api,所以,從iOS 8開始,需要解碼H.264視頻時,推薦使用系統提供的VideoToolbox來進行硬解

因為VideoToolbox解碼時的輸入是H.264數據,而通常看到的視頻流或者文件都是經過復用封裝之后的類似MP4格式的,所以在將數據交由VideoToolbox處理之前需要先進行解復用的操作來將H264數據抽取出來。目前比較通用的做法是使用FFmpeg來進行這個解復用的操作。

此處介紹通過FFmpeg讀取到視頻數據之后,再交由VideoToolbox解碼之前需要進行的一些操作。

1.1 H.264數據的結構

通常所說的H.264裸流,指的是由StartCode分割開來的一個個NALU組成的二進制序列,每個NALU一般來說就是一幀視頻圖像的數據(也有可能是多個NALU組成一幀圖像,或者該NALU是SPS、PPS等數據)

圖1 - H.264格式

如上圖所示,0x00 00 00 01四個字節為StartCode,在兩個StartCode之間的內容即為一個完整的NALU。
每個NALU的第一個字節包含了該NALU的類型信息,該字節的8個bit將其轉為二進制數據后,解讀順序為從左往右算,如下:
(1)第1位禁止位,值為1表示語法出錯
(2)第2~3位為參考級別
(3)第4~8為是nal單元類型
由此可知計算NALU類型時,只需將該字節的值與0x1F(二進制的0001 1111)相與,結果即為該NALU類型。
NALU類型有一下幾種:

圖2 - NALU類型

其中常見的有1、5、7、8、9幾種類型。

1.2 VideoToolbox可接收的數據格式

與通常所說的H.264數據格式有區別,VideoToolbox解碼時需要數據的H.264數據格式為AVC1格式,開始的4個字節不是StartCode,而是后面NALU的數據長度。
常見的封裝格式中mp4和flv格式封裝的是AVC1格式的H.264, ts格式中是以StartCode為起始的H.264。
如果原始數據是StartCode的格式,則需要將其轉換為AVC1的格式才能交給VideoToolbox進行解碼

1.3 SPS和PPS

SPS(序列參數集Sequence Parameter Set)和 PPS(圖像參數集Picture Parameter Set)是圖2中NALU類型為7、8的兩種NALU,其中包含了圖像編碼的各種參數信息,為解碼時必須的輸入。

二. 實踐

實踐過程中遇到過不少坑,大致如下。

2.1 SPS和PPS的獲取

如果是自己實現解復用來提取音視頻流中H.264數據,可以通過分析H.264數據中的NALU類型來獲取SPS和PPS。
但通常的做法是使用FFmpeg來實現解復用,此時調用avformat_open_input和avformat_find_stream_info函數后,可以在AVCodecContext結構中的extradata數據中獲取SPS和PPS數據
extradata為一個數據塊的地址指針,extradata_size指明了其長度,其中存儲的數據有兩種格式:
(1) 直接存儲SPS和PPS兩個NALU
(2) 存儲一個AVCDecoderConfigurationRecord格式的數據,該結構在標準文檔“ISO-14496-15 AVC file format”中有詳細說明,如下圖:

圖3 - AVCDecoderConfigurationRecord

實際使用過程中可以通過extradata中的開始幾個字節來判斷是其中存儲的是那種類型的數據(起始數據為00 00 00 01或00 00 01的為兩個NALU)

2.2 4字節還是3字節的StartCode

StartCode可以是4個字節的00 00 00 01,也可以是3個字節的00 00 01, 有資料說當一幀圖像被編碼為多個slice(即需要有多個NALU)時,每個NALU的StartCode為3個字節,否則為4個字節。但實際并不是所有編碼器都按照這個規定實現,所以在實際使用過程中,要對4個字節和3個字節的StartCode都進行一次判斷,包括上面說的extradata中的SPS和PPS,還有實際圖像的NALU。

2.3 FFmpeg中side_data的影響

如果使用FFmpeg對ts格式進行解復用操作,在av_read_frame讀取到一幀視頻數據之后,需要將數據轉換為AVC1的格式,但如果在FFmpeg中沒有對AVFormatContext結構的flags變量設置AVFMT_FLAG_KEEP_SIDE_DATA,那么獲取的AVPacket結構中的data地址中,保存的將不僅僅只有原始數據,它還將在末尾包含一個叫做side_data的數據(其實存儲的是MEPG2標準中定義的StreamID),這個數據會導致計算的NALU長度比實際要長,從而可能導致VideoToolbox無法解碼。
避免VideoToolbox解碼失敗的方法有兩種,任選其一即可:

  • 設置AVFormatContext的AVFMT_FLAG_KEEP_SIDE_DATA;
  • 調用av_packet_split_side_data將side_data從data數據中分離。

2.4 分辨率的變化

flv和ts格式的流都支持中途改變分辨率,所以在分辨率變化后需要重新初始化VideoToolbox的session,否則將會產生解碼錯誤。
碼流的分辨率發生變化(或者說編碼參數發生變化時),都會有SPS和PPS數據的更新,需要根據新的SPS和PPS信息重新建立解碼session。
ts流中更新的SPS和PPS數據和普通視頻數據一樣,正常解析數據即可獲取到新的SPS和PPS數據。
flv流或者rtmp流中的SPS和PPS數據的更新,位于FLV結構中一個叫做AVC sequence header的tag中,其中存儲的為圖3所示的一個AVCDecoderConfigurationRecord結構,需要從中提取出SPS和PPS數據。該數據在使用FFmpeg的av_read_frame獲取數據時,依然保存在AVPacket的side_data中。
獲取到SPS和PPS數據后,可以創建一個CMFormatDescriptionRef結構,然后使用VTDecompressionSessionCanAcceptFormatDescription函數判定原有session是否能解碼新的數據,如果返回值為假,則需要重建解碼session。
而新的視頻分辨率可以通過解析SPS數據來獲取。

三. SPS解析

SPS結構如下圖所示:


圖4 - SPS

根據SPS信息計算視頻分辨率如下

width = ((pic_width_in_mbs_minus1 +1)*16) - frame_crop_left_offset*2 - frame_crop_right_offset*2;
height = ((2 - frame_mbs_only_flag)* (pic_height_in_map_units_minus1 +1) * 16) - (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2);

圖4中descriptor中的描述意義如下

static int64_t
nal_bs_read_se(nal_bitstream *bs)
{
    int64_t ueVal = nal_bs_read_ue(bs);
    double k = ueVal;
    int64_t nValue = ceil(k/2);
    if(ueVal%2 == 0)
    {
        nValue = -nValue;
    }
    return nValue;
}

或者參考ffmpeg中對SPS解析的實現https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/h264_ps.c#L334

轉載請注明:
作者金山視頻云,首發簡書 Jianshu.com


如果準備使用多媒體移動端SDK,請參考https://github.com/ksvc

金山云SDK相關的QQ交流群:

  • 視頻云技術交流群:574179720
  • 視頻云iOS技術交流:621137661
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。