關於流媒體寫文件問題,確實搞起來比較頭疼,網上的有很多現成的解決方案,借用來借用去,總是無法滿足當前的需求,況且發現設備上傳過來的碼流數據的音頻存在重複時間戳和視頻幀卡頓等的問題,在寫文件的時候很容易就會音視頻不同步、只有音頻沒有視頻,或者只有視頻沒有音頻,或者視頻幀連續快速播放幾十秒,或音頻播放一段時間後無聲音等等問題。以前覺得1078部標上的那些比如數據類型、分包處理標記、Last Frame Interval等等這些字段,應該不怎麼需要,後來在開發過程中,逐漸的就一個個給用上了。
關於H264寫文件,H264文件要抽離出一個完整幀,所以,這時候就要將分包進行組合,就會用到分包處理標記,將合併後的完整幀,再通過mp4v2寫入mp4文件。
對於H264的視頻分辨率和幀率,如果你是固定的分辨率和幀率倒是好說,直接寫入或者配置好就可以了,如果是可變的,比如有子碼流和主碼流之分,比如終端設備可調節分辨率,那就稍微麻煩一點,但不是不能實現。在接收到第一個H264關鍵幀時,需要對其進行解析,主要是解析出裏面的SPS信息(包括分辨率和幀率),一般分辨率都是能獲取到的,幀率的話就未必了。像我這邊就只能獲取到分辨率,幀率獲取不到(尷尬不)。這個幀率,在後續對mp4文件進行初始化的時候是有可能會用到的,至於爲什麼說是有可能,這是因爲如果你是固定幀率,且通信狀況良好的終端設備,那在調用mp4v2的接口MP4AddH264VideoTrack()時,對sampleDuration直接寫上就好,後續在進行MP4WriteSample()時,不再需要寫duration。(注意:這個地方也是解決跳幀、丟幀的關鍵地方,另外一個關鍵地方則是時間戳)。如果你不是固定幀率,或者說通信狀況偶發性不好,那麼按照上面的方法,你的mp4文件將可能出現,視頻部分段快速播放(感覺像快進)、音頻只有開始一段有聲音,後續沒聲音了、音視頻不同步等等問題。在這種情況下,幀率就不管用了,就需要用上時間戳。所以,如果你需要支持可變幀率,那在調用MP4AddH264VideoTrack時,需要寫上MP4_INVALID_DURATION,後續在寫每一段視頻幀時,根據時間戳,調整duration。
以下爲解析分辨率和幀率的代碼,來自於互聯網,具體引用的哪一家的我給忘了(抱歉~):
INT h264_parse_sps(const BYTE *data, UINT dataSize, sps_info_struct *info)
{
if (!data || dataSize <= 0 || !info) return 0;
INT ret = 0;
BYTE *dataBuf = (BYTE*)malloc(dataSize);
memcpy(dataBuf, data, dataSize); //重新拷貝一份數據,防止移除競爭碼時對原數據造成影響
del_emulation_prevention(dataBuf, &dataSize);
sps_bit_stream bs = {0};
sps_bs_init(&bs, dataBuf, dataSize); //初始化SPS數據流結構體
u(&bs, 1); //forbidden_zero_bit
u(&bs, 2); //nal_ref_idc
UINT nal_unit_type = u(&bs, 5);
if (nal_unit_type == 0x7) { //Nal SPS Flag
info->profile_idc = u(&bs, 8);
u(&bs, 1); //constraint_set0_flag
u(&bs, 1); //constraint_set1_flag
u(&bs, 1); //constraint_set2_flag
u(&bs, 1); //constraint_set3_flag
u(&bs, 1); //constraint_set4_flag
u(&bs, 1); //constraint_set4_flag
u(&bs, 2); //reserved_zero_2bits
info->level_idc = u(&bs, 8);
ue(&bs); //seq_parameter_set_id
UINT chroma_format_idc = 1;
if (info->profile_idc == 100 || info->profile_idc == 110 || info->profile_idc == 122 ||
info->profile_idc == 244 || info->profile_idc == 44 || info->profile_idc == 83 ||
info->profile_idc == 86 || info->profile_idc == 118 || info->profile_idc == 128 ||
info->profile_idc == 138 || info->profile_idc == 139 || info->profile_idc == 134 || info->profile_idc == 135) {
chroma_format_idc = ue(&bs);
if (chroma_format_idc == 3) {
u(&bs, 1); //separate_colour_plane_flag
}
ue(&bs); //bit_depth_luma_minus8
ue(&bs); //bit_depth_chroma_minus8
u(&bs, 1); //qpprime_y_zero_transform_bypass_flag
UINT seq_scaling_matrix_present_flag = u(&bs, 1);
if (seq_scaling_matrix_present_flag) {
UINT seq_scaling_list_present_flag[8] = {0};
for (INT i=0; i<((chroma_format_idc != 3)?8:12); i++) {
seq_scaling_list_present_flag[i] = u(&bs, 1);
if (seq_scaling_list_present_flag[i]) {
if (i < 6) { //scaling_list(ScalingList4x4[i], 16, UseDefaultScalingMatrix4x4Flag[i])
} else { //scaling_list(ScalingList8x8[i - 6], 64, UseDefaultScalingMatrix8x8Flag[i - 6] )
}
}
}
}
}
ue(&bs); //log2_max_frame_num_minus4
UINT pic_order_cnt_type = ue(&bs);
if (pic_order_cnt_type == 0) {
ue(&bs); //log2_max_pic_order_cnt_lsb_minus4
} else if (pic_order_cnt_type == 1) {
u(&bs, 1); //delta_pic_order_always_zero_flag
se(&bs); //offset_for_non_ref_pic
se(&bs); //offset_for_top_to_bottom_field
UINT num_ref_frames_in_pic_order_cnt_cycle = ue(&bs);
INT *offset_for_ref_frame = (INT *)malloc((UINT)num_ref_frames_in_pic_order_cnt_cycle * sizeof(INT));
for (UINT i = 0; i<num_ref_frames_in_pic_order_cnt_cycle; i++) {
offset_for_ref_frame[i] = se(&bs);
}
free(offset_for_ref_frame);
}
ue(&bs); //max_num_ref_frames
u(&bs, 1); //gaps_in_frame_num_value_allowed_flag
UINT pic_width_in_mbs_minus1 = ue(&bs); //第36位開始
UINT pic_height_in_map_units_minus1 = ue(&bs); //47
UINT frame_mbs_only_flag = u(&bs, 1);
info->width = (INT)(pic_width_in_mbs_minus1 + 1) * 16;
info->height = (INT)(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16;
if (!frame_mbs_only_flag) {
u(&bs, 1); //mb_adaptive_frame_field_flag
}
u(&bs, 1); //direct_8x8_inference_flag
UINT frame_cropping_flag = u(&bs, 1);
if (frame_cropping_flag) {
UINT frame_crop_left_offset = ue(&bs);
UINT frame_crop_right_offset = ue(&bs);
UINT frame_crop_top_offset = ue(&bs);
UINT frame_crop_bottom_offset= ue(&bs);
//See 6.2 Source, decoded, and output picture formats
INT crop_unit_x = 1;
INT crop_unit_y = 2 - frame_mbs_only_flag; //monochrome or 4:4:4
if (chroma_format_idc == 1) { // 4:2:0
crop_unit_x = 2;
crop_unit_y = 2 * (2 - frame_mbs_only_flag);
} else if (chroma_format_idc == 2) { // 4:2:2
crop_unit_x = 2;
crop_unit_y = 2 - frame_mbs_only_flag;
}
info->width -= crop_unit_x * (frame_crop_left_offset + frame_crop_right_offset);
info->height -= crop_unit_y * (frame_crop_top_offset + frame_crop_bottom_offset);
}
UINT vui_parameters_present_flag = u(&bs, 1);
if (vui_parameters_present_flag) {
vui_para_parse(&bs, info);
}
ret = 1;
}
free(dataBuf);
return ret;
}
關於寫g711a寫mp4,採用的是g711a轉換成pcm,然後再轉換成aac,最後再將轉換後的AAC碼流寫入mp4文件。此處,g711a轉pcm,採用的軟件算法,如下爲部分代碼:
int g711_decode2(char *pRawData, unsigned char* pBuffer, int nBufferSize)
{
short *out_data = (short*)pRawData;
int i;
for (i = 0; i < nBufferSize; i++)
{
out_data[i] = decode(pBuffer[i]);
}
return nBufferSize*2;
}
轉換後的pcm,需要轉換成aac,採用了faac的第三方庫,這個庫就比較有意思,如果你的音頻是雙通道的,每次轉換應該是2048個單位,如果是單通道,應該是1024個單位,至於能不能每次320個(即每次轉換單個1078部標中的音頻數據包),這個我試了多次,沒有試通,這個地方也花費了不少時間。最後再將轉換後的AAC碼流,使用mp4v2庫,寫入文件中即可。
此處,網上有人說音頻幀是固定的,只要在初始化時MP4AddAudioTrack()的參數寫上1024或2048即可,這個方法確實可行,但也有一定的問題。即如果視頻幀不連續,或者像我這邊出現的音頻有重複或者回退的時間戳,音頻如果按照固定的duration,將會出現音視頻不同步問題。這時候就需要與視頻相同或者相似的處理方法,每次在寫入數據時,先計算出當前的時間戳,再計算duration,然後寫文件。
以上!
聯繫qq: 446733415