一、FFmpeg忽略了adaptation_field()数据

FFmpeg忽略了包含PCR值的adaptation_filed数据;

代码(libavformat/mpegts.c)分析如下:


/* 解析TS包 */

int handle_packet(MpegTSContext *ts, const uint8_t *packet)

{

  ...


  pid = AV_RB16(packet + 1) & 0x1fff;            //SYNTAX: PID

  is_start = packet[1] & 0x40;                        //SYNTAX: payload_unit_start_indicator

  ...


  /* continuity check (currently not used) */

  cc = (packet[3] & 0xf);                              //SYNTAX: continuity_counter   

  expected_cc = (packet[3] & 0x10) ? (tss->last_cc + 1) & 0x0f : tss->last_cc;

  cc_ok = (tss->last_cc < 0) || (expected_cc == cc);

  tss->last_cc = cc;          



  /* skip adaptation field */ 

  afc = (packet[3] >> 4) & 3;                      //SYNTAX: adaptation_field_control

  p = packet + 4;

  if (afc == 0) /* reserved value */

    return 0;

  if (afc == 2) /* adaptation field only */ 

    return 0;

  if (afc == 3) 

  {    

    /* skip adapation field */

    p += p[0] + 1;            

  }    

  ...

}


二、解码初始时间戳的计算

原理如下:

a. 分析阶段: 分析多个TS包,并找到第一个PES包的PTS,做为初始偏移量;

b. PTS置零:  分析与初始化阶段完成后,

             解码TS的第一个PES包,得到其PTS值,

             减去初始偏移量,使得第一个编码后帧的PTS为零;

c. DTS/PTS增量累加;



1. PTS置零代码分析

main(){

  |-- ...

  |-- parse_options(){

         |-- …

         |-- opt_input_file(){

                |-- … 

                   av_find_stream_info(ic);

                   timestamp = start_time;

                   timestamp += ic->start_time;

                   … 

                   input_files_ts_offset[nb_input_files] = 

                     input_ts_offset - (copy_ts ? 0 : timestamp);

                   …

             }

             …

      }

  |-- transcode(){

        |-- …

            for( ; received_sigterm == 0; ) {

              AVPacket pkt;

              …

              ret = av_read_frame(is, &pkt);

              …

              pkt.dts += av_rescale_q(input_files_ts_offset[nb_input_files],

                                     AV_TIME_BASE_Q, ist->st->time_base);

      }

}


三、编码音视频帧的DTS/PTS计算

音频帧的DTS/PTS计算:

一个音频帧(对于AAC来说, 是1024个采样点),

相对于音频采样率(如 44100个采样点/second = 44.1KHz)来说,

累加上每帧的增量(1024*1000/44100 = 23ms/frame)



st->time_base.den = 1000         //时钟基, 1 second = 1000 ms

frame_size        = 1024              //一帧 = 1024个采样点

st->pts           = {val=0, 

                          num=22050,  

den=44100}; // 音频采样率



av_frac_add(&st->pts, (int64_t)st->time_base.den * frame_size);


/* f.val = f.val + ((f.num + incr) / f->den) */

static void av_frac_add(AVFrac *f, int64_t incr)

{       

  int64_t num, den;


  num = f->num + incr;

  den = f->den;


  if (num < 0) 

  {

    f->val += num / den;

    num     = num % den;



    if (num < 0) 

    { 

      num += den;

      f->val--;

    }

  } 

  else if (num >= den) 

  {

    f->val += num / den;

    num = num % den;

  } 


  f->num = num;

}

st->pts           = {val=23,        // 计算后的时间戳

                         num=31750, // 上一帧未播放完的余值

                         den=44100}



视频帧的DTS/PTS计算:

一个视频帧,

相对于视频帧率来说(如 25 frames/second),

累加上每帧的增量(1000ms/25frames = 40ms/frame)


time_base.den     = 1000

time_base.num     = 1

st->pts           = {val=0, num=12, den=25}, 

av_frac_add(&st->pts, (int64_t)st->time_base.den * st->codec->time_base.num);



st->pts           = {val=40, num=12, den=25}



四、解码时间戳与编码时间戳的同步机制

正常的转码流程

(ffmpeg version 0.8.10 在ffmpeg.c的transcode函数

for(; received_sigterm == 0;){}

循环中):

step1. 解析PES包,得到时间戳、流索引、PES包长度等数据,并将这个PES包压入到PES包队列;

         见libavformat/mpegts.c函数 

int mpegts_push_data();


step2. 从PES包队列中取出一个PES包;

          见libavformat/utils.c函数

          int av_read_frame();

step3. 将这个PES包的PTS和/或DTS减去初始时间戳,

          见ffmpeg.c

          pkt.dts += av_rescale_q(input_files_ts_offset[ist->file_index], AV_TIME_BASE_Q, ist->st->time_base);

pkt.pts += av_rescale_q(input_files_ts_offset[ist->file_index], AV_TIME_BASE_Q, ist->st->time_base);


          并根据音频/视频流的采样率得到下一帧的PTS和/或DTS;

          见ffmpeg.c函数

          int output_packet();

          ist->next_pts = ist->pts = av_rescale_q(pkt->dts, ist->st->time_base, AV_TIME_BASE_Q);

          pkt_pts = av_rescale_q(pkt->pts, ist->st->time_base, AV_TIME_BASE_Q);



       如果本帧解码得到的时间戳和上一帧解码得到的时间戳的差值超过了设定的阈值,

       为了使输出的时间戳连续或同步,

       则需要调整, 如,

       视频帧时间戳不连续,则丢弃音频帧以同步

       音频帧时间戳不连续,则插件静音帧;

       或是其它的策略。


step4. 解码这个PES包中的音/视频帧, 并压入到相应的已解码音频/视频帧队列;

       见ffmpeg.c函数

       int output_packet();

       ret = avcodec_decode_audio3(ist->st->codec, samples, &decoded_data_size,&avpkt);

       ret = avcodec_decode_video2(ist->st->codec,&picture, &got_output, &avpkt);


step5. 以已解码音频/视频帧队列做为输入, 交错编码音频/视频帧,并将已编码数据压入到输出队列;

       见ffmpeg.c函数

       void do_video_out();

       void do_audio_out();       



step6. 根据要编码输出的音频/视频帧号及相应的采样率/帧率计算输出帧的时间戳;

       见libavformat/utils.c函数

       int compute_pkt_fields2();


step7. 将这个已编码音频/视频帧的数据和时间戳信息一起输出;

       见libavformat/flvenc.c函数

       int flv_write_packet()


step8. 没有到结束时,跳回到step1.



转码中的时间戳流程:

1. 解码TS包,

libavformat/mpegts.c的函数

int mpegts_push_data(MpegTSFilter *filter,

                     const uint8_t *buf, int buf_size, int is_start,

                     int64_t pos);

功能:

解析PES包, 获得时间戳等信息, 并取出负载数据组成ES流。



分析:

int mpegts_push_data(MpegTSFilter *filter,

                     const uint8_t *buf, int buf_size, int is_start,

                     int64_t pos)

{


  if (pes->header[0] == 0x00 &&                        //SYNTAX: packet_start_code_prefix 

      pes->header[1] == 0x00 && 

      pes->header[2] == 0x01)

  {

    code = pes->header[3] | 0x100;                    //SYNTAX: stream_id

    pes->total_size = AV_RB16(pes->header + 4); //SYNTAX: PES_packet_length


    /* 分配ES的空间 */

    pes->buffer = av_malloc(pes->total_size+FF_INPUT_BUFFER_PADDING_SIZE);



    if (code != 0x1bc && code != 0x1bf && /* program_stream_map, private_stream_2 */

        code != 0x1f0 && code != 0x1f1 && /* ECM, EMM */

        code != 0x1ff && code != 0x1f2 && /* program_stream_directory, DSMCC_stream */

        code != 0x1f8)                    /* ITU-T Rec.H.222.1 type E stream

    {

      flags = pes->header[7];                      //SYNTAX: PTS_DTS_flags

      if((flags & 0xc0) == ...)

      {

        pes->pts = ff_parse_pes_pts(r);        //SYNTAX: PTS[32...0]

        r += 5;

        pes->dts = ff_parse_pes_pts(r);        //SYNTAX: DTS[32...0]

        r += 5;

      }

      /* 取出PES的负载数据组成TS流 */

      memcpy(pes->buffer+pes->data_index, p, buf_size);

    }

  }

}


五、输入时间戳不边续时的处理机制

目的: 输入时间戳不连续,必须保证输出时间戳的连续。



1. 当视频时间戳连续,而音频时间戳不连续时

不强行修改时间戳,

用插入静音帧来实现重同步


一个奔跑的程序员