此章節分析承接上一章分析:
【十五】【vlc-android】vlc-sout流媒體輸出端源碼實作分析【Part 2】【02】
10.1.5、bs_read_ue實作分析:
// 【vlc/include/vlc_bits.h】
// 讀取指數哥倫布編碼,其實就是哥倫布解碼過程得到真實的int數值
/* Read unsigned Exp-Golomb code */
static inline uint_fast32_t bs_read_ue( bs_t * bs )
{
unsigned i = 0;
// 擷取h264片資料中第一個exp-golomb code 指數哥倫布編碼
// 是一種壓縮編碼算法(視訊編碼中有用到這個了,h264,avs)
// 原理舉例如下:
// K階哥倫布碼由如下步驟生成:
// a、 将數字以二進制的形式表達,去掉最低的K個bit之後+1;
// b、 計算留下的bit數,講此數減1,即需要在數字前添加的0的數目;
// c、 将a中去掉的K bit補回至最低位
// 實作為:讀取1個比特位數二進制值
// bs_read1見下面的分析
// 【未讀取也傳回0】,若傳回0,并且還未讀到最後一個位元組,
// 且i小于32即未讀取超過32比特位即4位元組資料時,再次進行讀取1個比特位數二進制值
// 直到讀到的1個比特位二進制值為1或末尾位元組或讀取超過了32位後結束讀取。
// 其實最終結果就是将碼流已讀取比特位置移動到目前比特位置二進制值為1的位置。
// [因為指數哥倫布編碼位元組二進制值中的前面是哥倫布編碼添加的0的值,是以需要跳過/去掉讀取]
while( bs_read1( bs ) == 0 && bs->p < bs->p_end && i < 31 )
i++;
// 1U表示無符号整數1
// 此處為哥倫布解碼處理不好描述,通過一個1階哥倫布編碼舉例來說明:
// 【int型數值2】編碼過程
// 如int型數值2的二進制表示為【10】,通過a步驟去掉二進制末尾【0】得到【1】二進制,然後加上1得到【10】,
// 在通過b步驟得到需要添加的0個數為:2 -1 = 1,即得到【010】,
// 然後通過c步驟在【010】後添加a步驟中去掉的【0】,得到最終哥倫布編碼結果為【0100】
// 【int型數值2】解碼過程
// 那麼可以知道通過上面的計算後,i的值肯定為1即數字前添加的0的個數值,
// 目前i_left資料指向的是【0100】中1的位置,
// 是以解碼時1U<<i即1U<<1值得到上面b步驟中的留下的bit數即為2,得到【10】,
// 然後減去1得到a中去掉最低的K個bit之後的二進制【01】,
// 根據bs_read方法的此前分析結果可知,方法傳回的值為讀取i位比特數二進制值,
// 然後轉為int值,此處i為1,并且根據上面的分析此時讀到的int值為1
// 是以【01】與其1相加後,最後得到【10】二進制,即就是int值為2的數值。是以解碼成功。
// 其他數值或K階編碼的解碼過程也類似。
return (1U << i) - 1 + bs_read( bs, i );
}
// 【vlc/include/vlc_bits.h】
static inline uint32_t bs_read1( bs_t *s )
{// 該方法從命名可以看出碼流讀取1個比特位數的二進制值
if( s->p < s->p_end )
{// 若目前資料指針已讀位置沒有到尾部,則進入
unsigned int i_result;
// 減去待讀取的1個比特位數
s->i_left--;
// 将目前位元組值和剩餘可用比特位數進行右移運算,得到讀取的高位二進制值,
// 然後和0x01進行與運算,進而擷取待讀取的1個比特位數的二進制表示的int值
i_result = ( *s->p >> s->i_left )&0x01;
if( s->i_left == 0 )
{// 若剛好讀取完目前位元組中剩餘位數,則進行下一個位元組的forward流程處理
// 該方法分析見上面小節相關分析
bs_forward( s, 1 );
// 因為是新位元組資料,是以可用位數變為8
s->i_left = 8;
}
return i_result;
}
// 未讀取到,則傳回0
return 0;
}
10.1.6、bs_read_se實作分析:
// 【vlc/include/vlc_bits.h】
/* Read signed Exp-Golomb code */
static inline int_fast32_t bs_read_se( bs_t *s )
{// NOTE:方法中的se和ue分别代表了int32_t和uint32_t兩種有無符号類型32位表示的int值
// 見上分析,先擷取到無符号32位整數int值
uint_fast32_t val = bs_read_ue( s );
// 然後和0x01進行與運算計算其二進制最後一位值是否為1,然後進行無符号轉有符号整數 TODO
return (val & 0x01) ? (int_fast32_t)((val + 1) / 2)
: -(int_fast32_t)(val / 2);
}
10.2、ActivateSets實作分析:
//【vlc/modules/packetizer/h264.c】
static void ActivateSets( decoder_t *p_dec, const h264_sequence_parameter_set_t *p_sps,
const h264_picture_parameter_set_t *p_pps )
{
decoder_sys_t *p_sys = p_dec->p_sys;
p_sys->p_active_pps = p_pps;
p_sys->p_active_sps = p_sps;
if( p_sps )
{
// H264碼流的h264檔次和等級
p_dec->fmt_out.i_profile = p_sps->i_profile;
p_dec->fmt_out.i_level = p_sps->i_level;
// 擷取圖像大小
// 見10.2.1小節分析
(void) h264_get_picture_size( p_sps, &p_dec->fmt_out.video.i_width,
&p_dec->fmt_out.video.i_height,
&p_dec->fmt_out.video.i_visible_width,
&p_dec->fmt_out.video.i_visible_height );
// vui表示視訊格式等額外資訊
// SAR值即采樣寬高比[分辨率]
if( p_sps->vui.i_sar_num != 0 && p_sps->vui.i_sar_den != 0 )
{
p_dec->fmt_out.video.i_sar_num = p_sps->vui.i_sar_num;
p_dec->fmt_out.video.i_sar_den = p_sps->vui.i_sar_den;
}
if( !p_dec->fmt_out.video.i_frame_rate ||
!p_dec->fmt_out.video.i_frame_rate_base )
{ // 存儲幀率的分子和分母int值【h264格式相關】,根據PTS值來估算幀率
// H.264碼流中一般沒有幀率,比特率資訊到是可以擷取,可參考碼流文法,
// 碼流有VUI資訊,其有個标志 timing_info_present_flag 若等于1,
// 則碼流中有num_units_in_tick 和 time_scale。
// 計算幀率framerate = time_scale/num_units_in_tick。
/* on first run == if fmt_in does not provide frame rate info */
/* If we have frame rate info in the stream */
if(p_sps->vui.b_valid &&
p_sps->vui.i_num_units_in_tick > 0 &&
p_sps->vui.i_time_scale > 1 )
{// 更新PTS的時間縮放機關【時間尺度】,其實用來計算PTS值的
date_Change( &p_sys->dts, p_sps->vui.i_time_scale,
p_sps->vui.i_num_units_in_tick );
}
/* else use the default num/den */
// 若上面未進入if,則會使用預設的PTS的時間尺度來估算幀率的分子、分母int值
p_dec->fmt_out.video.i_frame_rate = p_sys->dts.i_divider_num >> 1; /* num_clock_ts == 2 */
p_dec->fmt_out.video.i_frame_rate_base = p_sys->dts.i_divider_den;
}
if( p_dec->fmt_in.video.primaries == COLOR_PRIMARIES_UNDEF )
// 視訊資料的輸入格式資訊的基色類型未定義時
// 擷取基色資訊
//【通過查閱資料可知此處的基色類型其實是YUV與RGB之間的轉換矩陣标準的選擇】色彩空間和色彩範圍的處理
// 見10.2.2小節分析
h264_get_colorimetry( p_sps, &p_dec->fmt_out.video.primaries,
&p_dec->fmt_out.video.transfer,
&p_dec->fmt_out.video.space,
&p_dec->fmt_out.video.b_color_range_full );
if( p_dec->fmt_out.i_extra == 0 && p_pps )
{// 若輸出格式資訊中額外資料【即PPS SPS等】不存在,并且pps有資料,則進行指派處理
const block_t *p_spsblock = NULL;
const block_t *p_ppsblock = NULL;
// H264_SPS_ID_MAX最大為31
// 通過sps id比對對應的SPS資訊,然後将其block值指派
for( size_t i=0; i<=H264_SPS_ID_MAX && !p_spsblock; i++ )
if( p_sps == p_sys->sps[i].p_sps )
p_spsblock = p_sys->sps[i].p_block;
// H264_PPS_ID_MAX最大為255
// 通過pps id比對對應的PPS資訊,然後将其block值指派
for( size_t i=0; i<=H264_PPS_ID_MAX && !p_ppsblock; i++ )
if( p_pps == p_sys->pps[i].p_pps )
p_ppsblock = p_sys->pps[i].p_block;
if( p_spsblock && p_ppsblock )
{// SPS和PPS都不為空時則讀取它們的資料并儲存在fmt_out輸出格式資料中
// pps和sps總資料大小
size_t i_alloc = p_ppsblock->i_buffer + p_spsblock->i_buffer;
// 配置設定存儲資料的記憶體空間
p_dec->fmt_out.p_extra = malloc( i_alloc );
if( p_dec->fmt_out.p_extra )
{
uint8_t*p_buf = p_dec->fmt_out.p_extra;
// 記錄sps和pps總資料大小
p_dec->fmt_out.i_extra = i_alloc;
// 先存儲sps資料,再存儲pps資料
memcpy( &p_buf[0], p_spsblock->p_buffer, p_spsblock->i_buffer );
memcpy( &p_buf[p_spsblock->i_buffer], p_ppsblock->p_buffer,
p_ppsblock->i_buffer );
}
}
}
}
}
10.2.1、h264_get_picture_size實作分析:
//【vlc/modules/packetizer/h264_nal.c】
bool h264_get_picture_size( const h264_sequence_parameter_set_t *p_sps, unsigned *p_w, unsigned *p_h,
unsigned *p_vw, unsigned *p_vh )
{
// X、Y軸【寬高】裁剪機關
unsigned CropUnitX = 1;
// frame_mbs_only_flag 本句法元素等于 0 時表示本序列中所有圖像的編碼模式都是幀,
// 沒有其他編碼模式存在;本句法元素等于 1 時表示本序列中圖像的編碼模式可能是幀,
// 也可能是場或幀場自适應,某個圖像具體是哪一種要由其他句法元素決定。
unsigned CropUnitY = 2 - p_sps->frame_mbs_only_flag;
if( p_sps->b_separate_colour_planes_flag != 1 )
{// 色度不同顔色平面标志位不為1,即為0時
// idc[Importance indicator]
if( p_sps->i_chroma_idc > 0 )
{// 色度重要性訓示位大于0時
// 臨時寬高值裁剪機關
unsigned SubWidthC = 2;
unsigned SubHeightC = 2;
if( p_sps->i_chroma_idc > 1 )
{// 色度重要性訓示位大于1時
// 臨時高值裁剪機關改為1
SubHeightC = 1;
if( p_sps->i_chroma_idc > 2 )
// 色度重要性訓示位大于2時則将臨時寬值裁剪機關也改為1
SubWidthC = 1;
}
// 更新值
CropUnitX *= SubWidthC;
CropUnitY *= SubHeightC;
}
}
// pic_width_in_mbs_minus1 本句法元素加 1 後指明圖像寬度,以宏塊MB為機關:
// PicWidthInMbs = pic_width_in_mbs_minus1 + 1 通過這個句法元素解碼器可以
// 計算得到亮度分量以像素為機關的圖像寬度: PicWidthInSamplesL = PicWidthInMbs * 16
*p_w = 16 * p_sps->pic_width_in_mbs_minus1 + 16;
// pic_height_in_map_units_minus1 本句法元素加 1 後指明圖像高度:
// PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1
*p_h = 16 * p_sps->pic_height_in_map_units_minus1 + 16;
// 由上面可知,若本序列中所有圖像的編碼模式都是幀即frame_mbs_only_flag為0時,
// 需要将高度再乘以2,否則為1時就是其本身大小
*p_h *= ( 2 - p_sps->frame_mbs_only_flag );
// frame_cropping_flag用于指明解碼器是否要将圖像裁剪後輸出,如果是的話,
// 後面緊跟着的四個句法元素分别指出左右、上下裁剪的寬度。
// 将左右、上下裁剪的長度乘以裁剪機關值得到需要裁剪的真正寬高值,
// 然後用圖像原始寬高值減去裁剪的對應值,得到需要顯示的寬高值。
*p_vw = *p_w - ( p_sps->frame_crop.left_offset + p_sps->frame_crop.right_offset ) * CropUnitX;
*p_vh = *p_h - ( p_sps->frame_crop.bottom_offset + p_sps->frame_crop.top_offset ) * CropUnitY;
return true;
}
10.2.2、h264_get_colorimetry實作分析:
色彩空間和色彩範圍的處理【通過查閱資料可知此處的基色類型其實是YUV與RGB之間的轉換矩陣标準的選擇】
//【vlc/modules/packetizer/h264_nal.c】
bool h264_get_colorimetry( const h264_sequence_parameter_set_t *p_sps,
video_color_primaries_t *p_primaries,
video_transfer_func_t *p_transfer,
video_color_space_t *p_colorspace,
bool *p_full_range )
{
if( !p_sps->vui.b_valid )
return false;
// 主要就是将視訊格式中的色彩空間YUV基色類型轉為vlc中支援的類型
*p_primaries =
hxxx_colour_primaries_to_vlc( p_sps->vui.colour.i_colour_primaries );
// 類似上面的處理,即将色彩空間YUV轉RGB的轉換方法類型轉為vlc中支援的類型
*p_transfer =
hxxx_transfer_characteristics_to_vlc( p_sps->vui.colour.i_transfer_characteristics );
// 類似上面的處理,即将色彩空間YUV轉RGB的轉換矩陣系數為vlc中支援的系數類型
*p_colorspace =
hxxx_matrix_coeffs_to_vlc( p_sps->vui.colour.i_matrix_coefficients );
*p_full_range = p_sps->vui.colour.b_full_range;
return true;
}
//【vlc/modules/packetizer/hxxx_nal.h】
static inline video_color_primaries_t
hxxx_colour_primaries_to_vlc( enum hxxx_colour_primaries i_colour )
{
switch( i_colour )
{
case HXXX_PRIMARIES_BT470BG:
return COLOR_PRIMARIES_BT601_625;
case HXXX_PRIMARIES_BT601_525:
case HXXX_PRIMARIES_SMTPE_240M:
return COLOR_PRIMARIES_BT601_625;
case HXXX_PRIMARIES_BT709:
return COLOR_PRIMARIES_BT709;
case HXXX_PRIMARIES_BT2020:
return COLOR_PRIMARIES_BT2020;
case HXXX_PRIMARIES_BT470M:
case HXXX_PRIMARIES_RESERVED0:
case HXXX_PRIMARIES_UNSPECIFIED:
case HXXX_PRIMARIES_RESERVED3:
case HXXX_PRIMARIES_GENERIC_FILM:
case HXXX_PRIMARIES_SMPTE_ST_428:
default:
return COLOR_PRIMARIES_UNDEF;
}
}
10.3、IsFirstVCLNALUnit實作分析:
//【vlc/modules/packetizer/h264.c】
static bool IsFirstVCLNALUnit( const h264_slice_t *p_prev, const h264_slice_t *p_cur )
{
// 注譯:主編碼圖像的第一個VCL NAL單元的檢測
/* Detection of the first VCL NAL unit of a primary coded picture
* (cf. 7.4.1.2.4) */
if( p_cur->i_frame_num != p_prev->i_frame_num ||
p_cur->i_pic_parameter_set_id != p_prev->i_pic_parameter_set_id ||
p_cur->i_field_pic_flag != p_prev->i_field_pic_flag ||
!p_cur->i_nal_ref_idc != !p_prev->i_nal_ref_idc )
// 第一個VCL NAL單元判斷條件:
// 1、前後兩片【幀】資訊的幀序列号不相同時,代表是新的圖像資訊
// 2、pps圖像參數集的id不相同時
// 3、片中圖像場類型不同時
// 4、NAL重要性标志位不同時
// 以上4種情況任何一種發生都認為是新圖像資訊的開始【即新序列圖像中的第一個VCL NAL單元資訊】
return true;
if( (p_cur->i_bottom_field_flag != -1) &&
(p_prev->i_bottom_field_flag != -1) &&
(p_cur->i_bottom_field_flag != p_prev->i_bottom_field_flag) )
// 目前片資訊的底場辨別不一樣時,代表是新的圖像資訊
return true;
if( p_cur->i_pic_order_cnt_type == 0 &&
( p_cur->i_pic_order_cnt_lsb != p_prev->i_pic_order_cnt_lsb ||
p_cur->i_delta_pic_order_cnt_bottom != p_prev->i_delta_pic_order_cnt_bottom ) )
// 目前片資訊的POC為0模式并且POC圖像展示序号不一樣時,代表是新的圖像資訊
return true;
else if( p_cur->i_pic_order_cnt_type == 1 &&
( p_cur->i_delta_pic_order_cnt0 != p_prev->i_delta_pic_order_cnt0 ||
p_cur->i_delta_pic_order_cnt1 != p_prev->i_delta_pic_order_cnt1 ) )
// 目前片資訊的POC為0模式并且POC圖像展示序号不一樣時,代表是新的圖像資訊
return true;
if( ( p_cur->i_nal_type == H264_NAL_SLICE_IDR || p_prev->i_nal_type == H264_NAL_SLICE_IDR ) &&
( p_cur->i_nal_type != p_prev->i_nal_type || p_cur->i_idr_pic_id != p_prev->i_idr_pic_id ) )
// 目前幀片資訊的NALU類型有一個時IDR幀,并且不相同或IDR id不相同時,代表是新的圖像資訊
return true;
return false;
}
10.4、HxxxParse_AnnexB_SEI實作分析:
//【vlc/modules/packetizer/hxxx_sei.c】
void HxxxParse_AnnexB_SEI(const uint8_t *p_buf, size_t i_buf,
uint8_t i_header, pf_hxxx_sei_callback cb, void *cbdata)
{
// hxxx_strip_AnnexB_startcode該方法實作請見前面的分析
if( hxxx_strip_AnnexB_startcode( &p_buf, &i_buf ) )
// 若移除/跳過AnnexB流格式的起始碼成功時解析SEI單中繼資料
// 見下面的分析
HxxxParseSEI(p_buf, i_buf, i_header, cb, cbdata);
}
//【vlc/modules/packetizer/hxxx_sei.c】
void HxxxParseSEI(const uint8_t *p_buf, size_t i_buf,
uint8_t i_header, pf_hxxx_sei_callback pf_callback, void *cbdata)
{
bs_t s_ep3b;
bs_t s;
unsigned i_bitflow = 0;
bool b_continue = true;
char buf[256];
int i = 0;
if( i_buf <= i_header )
// 目前NAL頭部大小【機關位元組】大于等于目前資料塊負載資料大小時
return;
// 初始化s_ep3b碼流資訊對象,其跳過了NAL頭部資訊
bs_init( &s_ep3b, &p_buf[i_header], i_buf - i_header ); /* skip nal unit header */
// 記錄的目前碼流讀取位置
s_ep3b.p_fwpriv = &i_bitflow;
// 轉換功能【是否将模拟3位元組轉換為RBSP即原始位元組序列負載資料】
// 作用其實就是:解碼時脫殼操作即去掉NALU RBSP中的防僞0x03位元組【避免與起始碼競争】
// 見10.1.2小節分析
s_ep3b.pf_forward = hxxx_bsfw_ep3b_to_rbsp; /* Does the emulated 3bytes conversion to rbsp */
// 【bs_remain】見上面的分析即擷取資料的總可讀比特位數
// 【此處提到的最大65535個位元組數原因是bs_remain傳回int有符号類型值,若是32位則+正數最大2Exp16 - 1為65535】
/* While a NAL can technically be up to 65535 bytes, an SEI NAL
will never be anywhere near that size */
if (bs_remain(&s_ep3b) / 8 > sizeof(buf))
// 碼流中資料大于輸入的資料大小,理論上此處不會發生的
return;
while (bs_remain(&s_ep3b) > 0)
// 若有可讀比特位,則進行8比特數的讀取,
// 轉換每8位比特二進制值表示的int值儲存在char數組中【最大為256個位元組資料】
buf[i++] = bs_read(&s_ep3b, 8);
// 将産生的RBSP位元組資料解析為SEI資訊
/* Parse the resulting RBSP bytes as SEI */
// 初始化s碼流對象值
bs_init( &s, buf, i);
// bs_aligned實作:[見10.4.1小節分析]
// 判斷碼流每個位元組可讀比特位數是否是位元組對齊的即是否是8位元組的倍數
while( bs_remain( &s ) >= 8 && bs_aligned( &s ) && b_continue )
{// 碼流中可讀比特數必須大于等于8
/* Read type */
unsigned i_type = 0;
// 讀取SEI類型,該類型定義在【H.264/H.265 annex D】标準中
while( bs_remain( &s ) >= 8 )
{// 循環嘗試讀取類型值,從資料開始位置讀取每個位元組表示的值,
// 若不為0xff則表示找到了并退出查找,0xff表示無效值
const uint8_t i_byte = bs_read( &s, 8 );
i_type += i_byte;
if( i_byte != 0xff )
break;
}
/* Read size */
unsigned i_size = 0;
while( bs_remain( &s ) >= 8 )
{// 循環嘗試讀取SEI資料大小值,從資料開始位置讀取每個位元組表示的值,
// 若不為0xff則表示找到了并退出查找,0xff表示無效值
const uint8_t i_byte = bs_read( &s, 8 );
i_size += i_byte;
if( i_byte != 0xff )
break;
}
// 檢查SEI碼流資料可讀比特數,若小于8則退出循環處理
/* Check room */
if( bs_remain( &s ) < 8 )
break;
hxxx_sei_data_t sei_data;
sei_data.i_type = i_type;
// bs_pos方法實作傳回:( 8 * ( s->p - s->p_start ) + 8 - s->i_left )
// 即計算的是已讀資料比特數
// 儲存碼流資料中的起始偏移量【即已讀資料比特數】
/* Save start offset */
const unsigned i_start_bit_pos = bs_pos( &s );
switch( i_type )
{
/* Look for pic timing, do not decode locally */
case HXXX_SEI_PIC_TIMING:
{// 從SPS NALU單中繼資料中擷取SEI圖像時間
// picture timing sei提供了關于目前access unit的CPB removal delay和DPB output delay資訊。
// 一些分析軟體分析碼流的VBV Buffer模型時可以根據這些文法元素計算每個access unit
// 從CPB中的移出時間,驗證碼流是否有上下溢。
sei_data.p_bs = &s;
// 該方法調用的是:ParseSeiCallback
// 見10.5小節分析
b_continue = pf_callback( &sei_data, cbdata );
} break;
/* Look for user_data_registered_itu_t_t35 */
case HXXX_SEI_USER_DATA_REGISTERED_ITU_T_T35:
{// 視訊字幕标準處理流程
size_t i_t35;
uint8_t *p_t35 = malloc( i_size );
if( !p_t35 )
break;
for( i_t35 = 0; i_t35<i_size && bs_remain( &s ) >= 8; i_t35++ )
p_t35[i_t35] = bs_read( &s, 8 );
// 附加資訊
/* TS 101 154 Auxiliary Data and H264/AVC video */
if( i_t35 > 4 && p_t35[0] == 0xb5 /* United States */ )
{// 該字幕類型為美國US格式
if( p_t35[1] == 0x00 && p_t35[2] == 0x31 && /* US provider code for ATSC / DVB1 */
i_t35 > 7 )
{// ATSC / DVB1字幕編碼類型
switch( VLC_FOURCC(p_t35[3],p_t35[4],p_t35[5],p_t35[6]) )
{
case VLC_FOURCC('G', 'A', '9', '4'):
if( p_t35[7] == 0x03 )
{
sei_data.itu_t35.type = HXXX_ITU_T35_TYPE_CC;
sei_data.itu_t35.u.cc.i_data = i_t35 - 8;
sei_data.itu_t35.u.cc.p_data = &p_t35[8];
b_continue = pf_callback( &sei_data, cbdata );
}
break;
default:
break;
}
}
else if( p_t35[1] == 0x00 && p_t35[2] == 0x2f && /* US provider code for DirecTV */
p_t35[3] == 0x03 && i_t35 > 5 )
{// DirecTV字幕編碼類型
/* DirecTV does not use GA94 user_data identifier */
sei_data.itu_t35.type = HXXX_ITU_T35_TYPE_CC;
sei_data.itu_t35.u.cc.i_data = i_t35 - 5;
sei_data.itu_t35.u.cc.p_data = &p_t35[5];
b_continue = pf_callback( &sei_data, cbdata );
}
}
free( p_t35 );
} break;
case HXXX_SEI_FRAME_PACKING_ARRANGEMENT:
{// 幀包裝SEI資訊處理
// 幀包裝SEI(frame_packing_arrangement)包括作為優先級資訊的裁切忽略标志(frame_cropping_override_flag),
// 其訓示當顯示包裝圖像時是否優先使用幀包裝SEI。當訓示在顯示包裝圖像時優先使用幀包裝SEI時,
// 裁切忽略标志是1,而當在顯示包裝圖像時不優先使用幀包裝SEI時,裁切忽略标志是0。
// 讀取跳過無符号類型的指數哥倫布編碼值
bs_read_ue( &s );
// 讀取一個比特數的值
if ( !bs_read1( &s ) )
{// 若為0
// 讀取7個比特數表示的int值,即幀包裝SEI資訊類型
sei_data.frame_packing.type = bs_read( &s, 7 );
// 讀取跳過1個比特數
bs_read( &s, 1 );
// 幀包裝SEI資訊是否為第一個左端資訊
if( bs_read( &s, 6 ) == 2 ) /*intpr type*/
sei_data.frame_packing.b_left_first = false;
else
sei_data.frame_packing.b_left_first = true;
sei_data.frame_packing.b_flipped = bs_read1( &s );
// 幀包裝SEI資訊:是否為幀或場
sei_data.frame_packing.b_fields = bs_read1( &s );
sei_data.frame_packing.b_frame0 = bs_read1( &s );
}
else
// 若為1,表示取消幀包裝SEI類型資訊值的使用
sei_data.frame_packing.type = FRAME_PACKING_CANCEL;
} break;
/* Look for SEI recovery point */
case HXXX_SEI_RECOVERY_POINT:
{// SEI資訊圖像錯誤時視訊幀恢複點位置
// 讀取圖像錯誤時恢複幀數
sei_data.recovery.i_frames = bs_read_ue( &s );
// 是否精準比對
//bool b_exact_match = bs_read( &s, 1 );
// 是否損壞鍊【是否有哦中斷】
//bool b_broken_link = bs_read( &s, 1 );
// 片【視訊幀GOP計數改變值】
//int i_changing_slice_group = bs_read( &s, 2 );
// 該方法調用的是:ParseSeiCallback
// 見10.5小節分析
b_continue = pf_callback( &sei_data, cbdata );
} break;
case HXXX_SEI_MASTERING_DISPLAY_COLOUR_VOLUME:
{// 精确展示色域【更好的展示圖像細節】
if ( bs_remain( &s ) < (16*6+16*2+32+32) )
/* not enough data */
// 若沒有足夠資料讀取則退出switch
break;
// colorprim:Specify color primaries to use when converting to RGB,
// 可了解為色域,常見的有Rec.709(全高清廣播标準)、Rec.2020(4K/8K廣播标準BT.2020)、Adobe RGB、P3等
for ( size_t i = 0; i < 6 ; ++i)
// 色域值
sei_data.colour_volume.primaries[i] = bs_read( &s, 16 );
for ( size_t i = 0; i < 2 ; ++i)
// 白點位置,可網上查閱該部分知識點
sei_data.colour_volume.white_point[i] = bs_read( &s, 16 );
// 最大最小亮度值
sei_data.colour_volume.max_luminance = bs_read( &s, 32 );
sei_data.colour_volume.min_luminance = bs_read( &s, 32 );
// 該方法調用的是:ParseSeiCallback
// 見10.5小節分析
b_continue = pf_callback( &sei_data, cbdata );
} break;
case HXXX_SEI_CONTENT_LIGHT_LEVEL:
{// SEI内容亮度等級
if ( bs_remain( &s ) < (16+16) )
/* not enough data */
break;
// 最大content light level
sei_data.content_light_lvl.MaxCLL = bs_read( &s, 16 );
sei_data.content_light_lvl.MaxFALL = bs_read( &s, 16 );
// 該方法調用的是:ParseSeiCallback
// 見10.5小節分析
b_continue = pf_callback( &sei_data, cbdata );
} break;
default:
/* Will skip */
break;
}
// 儲存碼流資料中的資料讀取偏移量【即已讀資料比特數】
const unsigned i_end_bit_pos = bs_pos( &s );
/* Skip unsparsed content */
if( i_end_bit_pos - i_start_bit_pos > i_size * 8 ) /* Something went wrong with _ue reads */
// 若本次循環待讀資料大小都已讀取,則退出while
break;
// 若待讀取資料大小未讀取完畢,則跳過剩餘的待讀取資料大小,繼續下一次while循環讀取SEI資料處理
bs_skip( &s, i_size * 8 - ( i_end_bit_pos - i_start_bit_pos ) );
}
}
10.4.1、bs_aligned實作分析:
static inline bool bs_aligned( bs_t *s )
{// 判斷碼流每個位元組可讀比特位數是否是位元組對齊的即是否是8位元組的倍數
return s->i_left % 8 == 0;
}
10.5、ParseSeiCallback實作分析:
裁切忽略标志是1,而當在顯示包裝圖像時不優先使用幀包裝SEI時,裁切忽略标志是0。
if( p_dec->fmt_in.video.multiview_mode == MULTIVIEW_2D )
{// 輸出流多視圖模式格式:視訊多視圖模式為2D模式時,擷取輸出流的視圖模式
video_multiview_mode_t mode;
switch( p_sei_data->frame_packing.type )
{
case FRAME_PACKING_INTERLEAVED_CHECKERBOARD:
mode = MULTIVIEW_STEREO_CHECKERBOARD; break;
case FRAME_PACKING_INTERLEAVED_COLUMN:
mode = MULTIVIEW_STEREO_COL; break;
case FRAME_PACKING_INTERLEAVED_ROW:
mode = MULTIVIEW_STEREO_ROW; break;
case FRAME_PACKING_SIDE_BY_SIDE:
mode = MULTIVIEW_STEREO_SBS; break;
case FRAME_PACKING_TOP_BOTTOM:
mode = MULTIVIEW_STEREO_TB; break;
case FRAME_PACKING_TEMPORAL:
mode = MULTIVIEW_STEREO_FRAME; break;
case FRAME_PACKING_TILED:
default:
mode = MULTIVIEW_2D; break;
}
p_dec->fmt_out.video.multiview_mode = mode;
}
} break;
/* Look for SEI recovery point */
case HXXX_SEI_RECOVERY_POINT:
{// SEI資訊圖像錯誤時視訊幀恢複位置
if( !p_sys->b_recovered )
msg_Dbg( p_dec, "Seen SEI recovery point, %d recovery frames", p_sei_data->recovery.i_frames );
// 圖像錯誤時恢複幀數
p_sys->i_recovery_frame_cnt = p_sei_data->recovery.i_frames;
} break;
default:
/* Will skip */
break;
}
return true;
}
10.6、OutputPicture實作分析:
從打包子產品解碼對象中擷取輸出圖像資料
//【vlc/modules/packetizer/h264.c】
static block_t *OutputPicture( decoder_t *p_dec )
{
decoder_sys_t *p_sys = p_dec->p_sys;
block_t *p_pic = NULL;
block_t **pp_pic_last = &p_pic;
if( unlikely(!p_sys->frame.p_head) )
{// 若幀資料為空則清空處理,以下方法見前面已分析的流程
assert( p_sys->frame.p_head );
DropStoredNAL( p_sys );
ResetOutputVariables( p_sys );
cc_storage_reset( p_sys->p_ccs );
return NULL;
}
// 綁定已比對或參考的PPS和SPS NALU單元資訊,均不能為空
/* Bind matched/referred PPS and SPS */
const h264_picture_parameter_set_t *p_pps = p_sys->p_active_pps;
const h264_sequence_parameter_set_t *p_sps = p_sys->p_active_sps;
if( !p_pps || !p_sps )
{
DropStoredNAL( p_sys );
ResetOutputVariables( p_sys );
cc_storage_reset( p_sys->p_ccs );
return NULL;
}
if( !p_sys->b_recovered && p_sys->i_recoveryfnum == UINT_MAX &&
p_sys->i_recovery_frame_cnt == UINT_MAX && p_sys->slice.type == H264_SLICE_TYPE_I )
{
// 注譯:沒有辦法在I片【幀】錯誤時使用SEI資訊,僅僅通過I片【幀】同步
/* No way to recover using SEI, just sync on I Slice */
// 是以這種情況下不能使用SEI,即标記目前已恢複狀态為true,讓其後面不用再處理恢複流程,隻通過I幀同步
p_sys->b_recovered = true;
}
// I幀需要sps pps資訊【NALU單元】
bool b_need_sps_pps = p_sys->slice.type == H264_SLICE_TYPE_I &&
p_sys->p_active_pps && p_sys->p_active_sps;
/* Handle SEI recovery */
if ( !p_sys->b_recovered && p_sys->i_recovery_frame_cnt != UINT_MAX &&
p_sys->i_recoveryfnum == UINT_MAX )
{// 該情況處理SEI恢複資訊
// 錯誤恢複幀數
p_sys->i_recoveryfnum = p_sys->slice.i_frame_num + p_sys->i_recovery_frame_cnt;
// 錯誤恢複開始幀計數值
p_sys->i_recoverystartfnum = p_sys->slice.i_frame_num;
b_need_sps_pps = true; /* SPS/PPS must be inserted for SEI recovery */
// 注譯:使用SEI資訊進行錯誤恢複,預滾i_recovery_frame_cnt個參考【幀】圖像
// 備注:預滾(Preroll)概念【或預緩存】:一個sink元素隻有當有一個buffer被緩沖到sink pad裡面時,
// 才能夠完成到PAUSED狀态的改變,這個過程就被稱為預滾(Preroll),這樣做是為了能夠盡快的進入到PLAYING狀态,
// 以免給使用者造成視覺上的延遲。
// 預滾(Preroll)在音視訊同步方面是非常關鍵的,確定不會有buffer被sink元素抛棄。
msg_Dbg( p_dec, "Recovering using SEI, prerolling %u reference pics", p_sys->i_recovery_frame_cnt );
}
if( p_sys->i_recoveryfnum != UINT_MAX )
{// 若恢複幀數不為預設值
assert(p_sys->b_recovered == false);
// 最大幀數
const unsigned maxFrameNum = 1 << (p_sps->i_log2_max_frame_num + 4);
if( ( p_sys->i_recoveryfnum > maxFrameNum &&
p_sys->slice.i_frame_num < p_sys->i_recoverystartfnum &&
p_sys->slice.i_frame_num >= p_sys->i_recoveryfnum % maxFrameNum ) ||
( p_sys->i_recoveryfnum <= maxFrameNum &&
p_sys->slice.i_frame_num >= p_sys->i_recoveryfnum ) )
{// 若條件成立,則表示【從SEI恢複點恢複完成】,是以将恢複點功能标記設為已處理完成
// 備注:vlc中支援SEI Recovery Point功能的
p_sys->i_recoveryfnum = UINT_MAX;
p_sys->b_recovered = true;
msg_Dbg( p_dec, "Recovery from SEI recovery point complete" );
}
}
// 若有需要則收集PPS/SPS資訊到p_xpsnal資料塊中
/* Gather PPS/SPS if required */
block_t *p_xpsnal = NULL;
block_t **pp_xpsnal_tail = &p_xpsnal;
if( b_need_sps_pps || p_sys->b_new_sps || p_sys->b_new_pps )
{
for( int i = 0; i <= H264_SPS_ID_MAX && (b_need_sps_pps || p_sys->b_new_sps); i++ )
{
if( p_sys->sps[i].p_block )
// SPS不為空時
// 【block_Duplicate】備份block即資料拷貝到新block對象中。
// 該方法的實作前面第6小節中已分析,就是将新block資料塊連結到【p_xpsnal】總連結清單的末尾
block_ChainLastAppend( &pp_xpsnal_tail, block_Duplicate( p_sys->sps[i].p_block ) );
}
for( int i = 0; i < H264_PPS_ID_MAX && (b_need_sps_pps || p_sys->b_new_pps); i++ )
{
if( p_sys->pps[i].p_block )
// 同上實作原理
block_ChainLastAppend( &pp_xpsnal_tail, block_Duplicate( p_sys->pps[i].p_block ) );
}
}
// 現在重建NAL序列資料,插入PPS/SPS資訊(如果有的話)
/* Now rebuild NAL Sequence, inserting PPS/SPS if any */
if( p_sys->leading.p_head &&
(p_sys->leading.p_head->i_flags & BLOCK_FLAG_PRIVATE_AUD) )
{// Access Unit Delimiter通路單元分隔符:
// 一般文檔沒有對AUD進行描叙,其實這是一個幀開始的标志,位元組順序為:00 00 00 01 09 f0
// 從結構上看,有start code, 是以确實是一個NALU,類型09在H264定義裡就是AUD(分割器)。
// 大部分播放器可以在沒有AUD的情況下正常播放。
// 此處取出leading頭資料塊,将其放入【pp_pic_last】指向的【p_pic】總資料塊連結清單尾部
// 備注:此時【p_pic】指向連結清單為null,是以【p_pic】直接指向了【p_au】的資料塊
block_t *p_au = p_sys->leading.p_head;
p_sys->leading.p_head = p_au->p_next;
p_au->p_next = NULL;
block_ChainLastAppend( &pp_pic_last, p_au );
}
if( p_xpsnal )
// 在上面AU分隔符資料塊連結清單尾部加入上面的SPS和PPS組成的單連結清單【p_xpsnal】
block_ChainLastAppend( &pp_pic_last, p_xpsnal );
if( p_sys->leading.p_head )
// 同上分析,在【pp_pic_last】指向的【p_pic】總資料塊連結清單尾部加入【p_sys->leading.p_head】資料
block_ChainLastAppend( &pp_pic_last, p_sys->leading.p_head );
assert( p_sys->frame.p_head );
if( p_sys->frame.p_head )
// 同上分析,在【pp_pic_last】指向的【p_pic】總資料塊連結清單尾部加入【p_sys->frame.p_head】視訊幀資料
block_ChainLastAppend( &pp_pic_last, p_sys->frame.p_head );
// 上面已經擷取完所有關于H264視訊碼流資料,
// 是以此處重置清空這些資料塊鍊指針指向NULL
/* Reset chains, now empty */
p_sys->frame.p_head = NULL;
p_sys->frame.pp_append = &p_sys->frame.p_head;
p_sys->leading.p_head = NULL;
p_sys->leading.pp_append = &p_sys->leading.p_head;
// 将上面資料塊連結清單資料聚集/打包成一個資料塊block對象中儲存
// 見10.6.1小節分析
p_pic = block_ChainGather( p_pic );
if( !p_pic )
{// 目前圖像資料為空則失敗處理
ResetOutputVariables( p_sys );
cc_storage_reset( p_sys->p_ccs );
return NULL;
}
// MASK值為0xff000000,~MASK取反為0x00ffffff,
// 然後和目前flags值進行與運算,目的是清除高位【子產品私有】的flags
/* clear up flags gathered */
p_pic->i_flags &= ~BLOCK_FLAG_PRIVATE_MASK;
// 注譯:對PTS修正,交錯幀場(多個AU/塊)
/* for PTS Fixup, interlaced fields (multiple AU/block) */
// OC: order count序列号, POC: picture order count圖像顯示序列号
// tFOC: TopFieldOrderCnt表示頂場POC,bFOC:BottomFieldOrderCnt表示底場POC
int tFOC = 0, bFOC = 0, PictureOrderCount = 0;
// 計算目前片【幀/場】圖像POC參數值
// 見10.6.2小節分析
h264_compute_poc( p_sps, &p_sys->slice, &p_sys->pocctx, &PictureOrderCount, &tFOC, &bFOC );
// 擷取時鐘時間戳計數
// 見10.6.3小節分析
unsigned i_num_clock_ts = h264_get_num_ts( p_sps, &p_sys->slice, p_sys->i_pic_struct, tFOC, bFOC );
// frame_mbs_only_flag 本句法元素等于 0 時表示本序列中所有圖像的編碼模式都是幀,
// 沒有其他編碼模式存在;本句法元素等于 1 時表示本序列中圖像的編碼模式可能是幀,
// 也可能是場或幀場自适應,某個圖像具體是哪一種要由其他句法元素決定。
if( p_sps->frame_mbs_only_flag == 0 && p_sps->vui.b_pic_struct_present_flag )
{// 幀編碼模式并且視訊可用資訊VUI中的圖像結構類型存在标志位存在時
switch( p_sys->i_pic_struct )
{
// 注譯:頂場和底場片資訊
/* Top and Bottom field slices */
// 頂場Slice
case 1:
// 底場Slice
case 2:
// 增加辨別:目前圖像資料塊包含單場【隔行幀】
p_pic->i_flags |= BLOCK_FLAG_SINGLE_FIELD;
// i_bottom_field_flag:底場标志位
// 1、存在則增加辨別為:BLOCK_FLAG_BOTTOM_FIELD_FIRST即隔行幀的第一個底場
// 2、不存在則辨別為:BLOCK_FLAG_TOP_FIELD_FIRST即隔行幀的第一個頂場
p_pic->i_flags |= (!p_sys->slice.i_bottom_field_flag) ? BLOCK_FLAG_TOP_FIELD_FIRST
: BLOCK_FLAG_BOTTOM_FIELD_FIRST;
break;
// 注譯:以下每種類型片資訊都包含了更多場即不止一個場
/* Each of the following slices contains multiple fields */
case 3:
p_pic->i_flags |= BLOCK_FLAG_TOP_FIELD_FIRST;
break;
case 4:
p_pic->i_flags |= BLOCK_FLAG_BOTTOM_FIELD_FIRST;
break;
case 5:
p_pic->i_flags |= BLOCK_FLAG_TOP_FIELD_FIRST;
break;
case 6:
p_pic->i_flags |= BLOCK_FLAG_BOTTOM_FIELD_FIRST;
break;
default:
break;
}
}
// 注譯:設定視訊幀PTS和DTS給目前block資料塊時戳資訊
/* set dts/pts to current block timestamps */
p_pic->i_dts = p_sys->i_frame_dts;
p_pic->i_pts = p_sys->i_frame_pts;
// 注譯:若目前資料塊進行了分割/分片【多個AU通路單中繼資料或多個資料塊】,則檢查修複丢失的DTS時戳資訊
/* Fixup missing timestamps after split (multiple AU/block)*/
// 檢查目前圖像DTS
if( p_pic->i_dts <= VLC_TS_INVALID )
p_pic->i_dts = date_Get( &p_sys->dts );
if( p_sys->slice.type == H264_SLICE_TYPE_I )
// 目前為I片則初始化該值【前一個參考圖像過期POC值】為無效即0
p_sys->prevdatedpoc.pts = VLC_TS_INVALID;
if( p_pic->i_pts == VLC_TS_INVALID )
{// 目前圖像PTS無效
if( p_sys->prevdatedpoc.pts > VLC_TS_INVALID &&
date_Get( &p_sys->dts ) != VLC_TS_INVALID )
{// 不為I片【幀】時,此處實作:PTS根據DTS來計算得出
date_t pts = p_sys->dts;
// 該過期POC時間設定給pts
date_Set( &pts, p_sys->prevdatedpoc.pts );
// POC差量【此處得到的是樣本數】:頂場POC - 前一個過期的POC中的個數
int diff = tFOC - p_sys->prevdatedpoc.num;
if( diff > 0 )
// 增加差量時間
// 該方法的分析見此前章節中已有的分析
date_Increment( &pts, diff );
else
// 減少差量時間
date_Decrement( &pts, -diff );
// 根據上面的計算得到目前圖像合适的PTS
p_pic->i_pts = date_Get( &pts );
// 注譯:非單調遞增的dts在一些視訊33333 33333…35000
/* non monotonically increasing dts on some videos 33333 33333...35000 */
if( p_pic->i_pts < p_pic->i_dts )
// 若PTS小于DTS則将其和DTS相同
p_pic->i_pts = p_pic->i_dts;
}
/* In case there's no PTS at all */
else if( CanSwapPTSwithDTS( &p_sys->slice, p_sps ) )
{// 根據注釋此處沒有PTS值,是以直接和DTS相同即可
// 該方法實作見10.6.4小節分析
p_pic->i_pts = p_pic->i_dts;
}
else if( p_sys->slice.type == H264_SLICE_TYPE_I &&
date_Get( &p_sys->dts ) != VLC_TS_INVALID )
{// 為I幀并且DTS有效
// IDR幀沒有PTS,圖像顯示時間完全是瞎的,
// 是以此處預設處理為DTS值基礎上加上兩個樣本資料的時長即為PTS
/* Hell no PTS on IDR. We're totally blind */
date_t pts = p_sys->dts;
date_Increment( &pts, 2 );
p_pic->i_pts = date_Get( &pts );
}
}
else if( p_pic->i_dts == VLC_TS_INVALID &&
CanSwapPTSwithDTS( &p_sys->slice, p_sps ) )
{// 目前圖像PTS有效時但DTS無效并且可以将PTS和DTS交換
p_pic->i_dts = p_pic->i_pts;
if( date_Get( &p_sys->dts ) == VLC_TS_INVALID )
// 是以檢查p_sys中儲存的dts值若為無效,則儲存目前圖像的dts
date_Set( &p_sys->dts, p_pic->i_pts );
}
if( p_pic->i_pts > VLC_TS_INVALID )
{// 若圖像PTS有效,則指派【目前圖像PTS和POC值】給該對象對應字段【即前一個過期POC資訊】儲存起來
p_sys->prevdatedpoc.pts = p_pic->i_pts;
p_sys->prevdatedpoc.num = PictureOrderCount;
}
if( p_pic->i_length == 0 )
{// 若目前圖像播放時長為空,則計算
// p_sys儲存的下個解碼DTS時間點【可能為目前圖像PTS顯示時間點】
date_t next = p_sys->dts;
// 增加時戳資訊組個數的樣本資料時長作為目前圖像應該播放結束的時間點
// 【時戳資訊文法元素的内容說明源時間,拍攝時間或理想的播放時間】
date_Increment( &next, i_num_clock_ts );
// 是以,目前圖像播放時長 = 目前圖像播放結束時間點 - 目前圖像PTS播放時間點
p_pic->i_length = date_Get( &next ) - date_Get( &p_sys->dts );
}
// Debug時可以打開調試
#if 0
msg_Err(p_dec, "F/BOC %d/%d POC %d %d rec %d flags %x ref%d fn %d fp %d %d pts %ld len %ld",
tFOC, bFOC, PictureOrderCount,
p_sys->slice.type, p_sys->b_recovered, p_pic->i_flags,
p_sys->slice.i_nal_ref_idc, p_sys->slice.i_frame_num, p_sys->slice.i_field_pic_flag,
p_pic->i_pts - p_pic->i_dts, p_pic->i_pts % (100*CLOCK_FREQ), p_pic->i_length);
#endif
// 更新儲存目前的DTS時間,用于修正下一個圖像幀的時戳
/* save for next pic fixups */
if( date_Get( &p_sys->dts ) != VLC_TS_INVALID )
{// 目前全局記錄的DTS最新值有效
if( p_sys->i_next_block_flags & BLOCK_FLAG_DISCONTINUITY )
// 下個資料塊标志位為非連續幀塊資料,則清空緩存的DTS值,
// 這樣就會在下個資料塊處理時,不用同步上一個資料的時間戳
date_Set( &p_sys->dts, VLC_TS_INVALID );
else
// 若是連續幀資料,則根據目前圖像時戳資訊組個數作為樣本個數來估算下個DTS時間點
date_Increment( &p_sys->dts, i_num_clock_ts );
}
if( p_pic )
{// 目前圖像标志位加上目前【下一個塊标志位】
p_pic->i_flags |= p_sys->i_next_block_flags;
// 并将下一次的指派為0【未定義标志位】
p_sys->i_next_block_flags = 0;
}
switch( p_sys->slice.type )
{// 判斷目前圖像資料塊圖像片【幀】類型
case H264_SLICE_TYPE_P:
// 添加辨別:P幀
p_pic->i_flags |= BLOCK_FLAG_TYPE_P;
break;
case H264_SLICE_TYPE_B:
// B幀
p_pic->i_flags |= BLOCK_FLAG_TYPE_B;
break;
case H264_SLICE_TYPE_I:
// I幀
p_pic->i_flags |= BLOCK_FLAG_TYPE_I;
default:
break;
}
if( !p_sys->b_recovered )
{// 若還未進行恢複點處理,則标記目前圖像狀态
if( p_sys->i_recoveryfnum != UINT_MAX ) /* recovering from SEI */
// 若是從恢複點SEI資訊中恢複的,則标記目前圖像為預滾【預緩存】圖像
p_pic->i_flags |= BLOCK_FLAG_PREROLL;
else
// 否則标記目前圖像需要Drop丢棄掉
p_pic->i_flags |= BLOCK_FLAG_DROP;
}
// 此處也是清除該子產品私有辨別【BLOCK_FLAG_PRIVATE_AUD】
p_pic->i_flags &= ~BLOCK_FLAG_PRIVATE_AUD;
// 在圖像輸出之後進行重置目前儲存打包器解碼對象的相關值
/* reset after output */
ResetOutputVariables( p_sys );
// 送出字幕存儲相關資訊給p_ccs
/* CC */
cc_storage_commit( p_sys->p_ccs, p_pic );
return p_pic;
}
10.6.1、block_ChainGather實作分析:【請檢視下一章節分析】
【十五】【vlc-android】vlc-sout流媒體輸出端源碼實作分析【Part 2】【04】