购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1
音视频时间

本节介绍音视频涉及的时间相关概念,首先描述什么是码率、帧率、采样率,以及如何获取音视频文件中的码率、帧率和采样率;然后叙述什么是音视频的时间基准,以及如何获取音视频文件中的时间基准;最后阐述什么是音视频的播放时间戳和解码时间戳,以及如何根据时间基准计算一个数据帧的增量时间戳。

3.1.1 帧率和采样率

根据单位时间内产生变化的数据类型,音视频在传输过程中存在三个重要的速率,分别是码率、帧率和采样率。其中码率描述了在单位时间内传输的数据大小,它的单位是bit/s,也叫bps(位每秒,全称bits per second,每秒传输的位数),因为bit音译成比特,所以bit/s也可译为比特每秒,连带着码率也被叫作比特率了。

帧率专用于视频,指的是视频帧连续显示的频率,也就是每秒会展现多少幅视频画面,它的单位是fps(帧每秒,全称frames per second,每秒传输的帧数)。比如电影放映的标准是每秒24帧,那么该电影的帧率就是24fps。帧率越高,人眼感知的视频画面就越流畅。常见的视频制式及其对应帧率的关系说明见表3-1。

表3-1 常见的视频制式及其对应帧率

与帧率含义相似的另一个概念是刷新率。它们的区别在于:帧率是针对传输的,表达的是每秒传输的帧数,其单位是fps;刷新率是针对显示的,表达的是每秒的刷新次数,其单位是Hz(赫兹)。刷新率越高,显示的图像画面就越稳定,越不容易闪烁。常见的显示场合及其对应刷新率的关系说明见表3-2。

表3-2 常见的显示场合及其对应刷新率

人眼看到的视频画面质量与帧率、刷新率同时关联,最终的视觉感受取决于二者的最小值。因为如果帧率上不去,刷新率再高也没用;反之,如果刷新率较低,那么即使加大帧率也不行。比如一个视频文件的帧率是15fps,那么无论是在60Hz刷新率还是在90Hz刷新率的手机上,该视频每秒都只能展现15帧画面,流畅度没有差别。又如一台显示器的刷新率是60Hz,原本帧率为60fps的视频,即使它的帧率提高到90fps,这台显示器每秒也只能刷新60次,超额的帧率部分就丢失了。因此,在实际应用中,屏幕的刷新率总是比视频的帧率要大一些,这样看视频既不浪费,也足够用了。

采样率多用于音频,也称采样频率、采样速度,指的是单位时间内从连续信号采集离散样本的次数,它的单位名称是Hz。采样率越大,采集到的音频信号就越连贯,也越清晰。常见的音频采样率及其应用场合对照关系见表3-3。

表3-3 常见的音频采样率及其应用场合

FFmpeg的数据流结构AVStream包含上述的比特率、帧率、采样率信息,主要看AVStream里面的r_frame_rate和codecpar两个字段。其中r_frame_rate字段为AVRational类型,存放着帧率信息,它的结构定义如下:

    typedef struct AVRational{
        int num;         // Numerator,分子
        int den;         // Denominator,分母
    } AVRational;        // Rational,定量

可见AVRational是个分数结构,对于帧率来说,其值等于分子除以分母,也就是r_frame_rate.num/r_frame_rate.den。之所以引入分数结构,是因为有些数值属于除不尽的无限小数,如果使用double类型保存这种数值,就会产生精度损失,一旦经过多次运算,累积的精度损失就会影响数值准确性,故而通过分数结构表达无限小数,避免精度损失的传导扩大。

codecpar字段则为AVCodecParameters指针类型,存放着音视频的详细参数信息。AVCodecParameters结构的常见字段说明如下。

· bit_rate:音视频的比特率、码率,单位为比特每秒(bit/s)。

· width:视频画面的宽度。

· height:视频画面的高度。

· frame_size:音频帧的大小,也就是每帧音频的采样数量。

· sample_rate:音频的采样率,单位为赫兹(Hz)。

· ch_layout:音频的声道信息,该字段为AVChannelLayout类型,其下的nb_channels字段表示声道数量。

根据以上结构定义可知,音视频的码率为codecpar->bit_rate,视频的帧率为r_frame_rate.num/r_frame_rate.den,音频的采样率为codecpar->sample_rate(注意:这里的->为C语言指针类型专用的指示操作符,不能用向右箭头→代替)。

既然知道了视频的帧率,就容易计算出每帧视频的持续时间,假设某个视频文件的帧率为25fps(每秒25帧),则每帧视频的持续时间=1000÷25=40ms(40毫秒)。那么每帧音频的持续时间的计算能否依葫芦画瓢呢?比如常见的音频采样率为44100Hz,单个离散样本的采集时间段=1000÷44100≈0.023毫秒。然而这个0.023毫秒并非一帧音频的持续时间,因为一个音频帧包含若干离散样本,依据不同的音频标准,音频帧内含的样本数量各不相同。例如MP3标准规定每帧音频包含1152个样本,AAC标准规定每帧音频包含1024个样本,因此音频帧的持续时间应当等于一个样本的持续时间乘以每帧音频的样本数量。

以MP3标准为例,每帧MP3音频包含1152个样本,且它的采样率为44100Hz。因此,一帧MP3音频的持续时间=1152×1000÷44100≈26.1ms。再来计算AAC标准,每帧AAC音频包含1024个样本,且它的采样率为44100Hz。因此,一帧AAC音频的持续时间=1024×1000÷44100≈23.2ms。现在明白了,MP3音频的每帧持续时间26.7毫秒,原来是这么计算的。由于AAC音频的每帧持续时间为23.2毫秒,它的时间切片比MP3更小,因此理论上音频质量会更高。

综合上面的结构介绍和算式推导,编写FFmpeg代码,可以提取音视频的码率、帧率、采样率,还可以分别计算每帧音视频的持续时间,示例代码片段如下(完整代码见chapter03/fps.c)。

     // 找到视频流的索引
     int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
     if (video_index >= 0) {
         AVStream *video_stream = fmt_ctx->streams[video_index];
         AVCodecParameters *video_codecpar = video_stream->codecpar;
         // 计算帧率,每秒有几个视频帧
         int fps = video_stream->r_frame_rate.num/video_stream->r_frame_rate.den;
         av_log(NULL, AV_LOG_INFO, "video_codecpar bit_rate=%d\n",
video_codecpar->bit_rate);
         av_log(NULL, AV_LOG_INFO, "video_codecpar width=%d\n", video_codecpar->width);
         av_log(NULL, AV_LOG_INFO, "video_codecpar height=%d\n", video_codecpar->height);
         av_log(NULL, AV_LOG_INFO, "video_codecpar fps=%d\n", fps);
         int per_video = round(1000 / fps);  // 计算每个视频帧的持续时间
         av_log(NULL, AV_LOG_INFO, "one video frame's duration is %dms\n", per_video);
     }
     // 找到音频流的索引
     int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
     if (audio_index >= 0) {
         AVStream *audio_stream = fmt_ctx->streams[audio_index];
         AVCodecParameters *audio_codecpar = audio_stream->codecpar;
         av_log(NULL, AV_LOG_INFO, "audio_codecpar bit_rate=%d\n",
audio_codecpar->bit_rate);
         av_log(NULL, AV_LOG_INFO, "audio_codecpar frame_size=%d\n",
audio_codecpar->frame_size);
         av_log(NULL, AV_LOG_INFO, "audio_codecpar sample_rate=%d\n",
audio_codecpar->sample_rate);
         av_log(NULL, AV_LOG_INFO, "audio_codecpar nb_channels=%d\n",
audio_codecpar->ch_layout.nb_channels);
         // 计算音频帧的持续时间。frame_size为每个音频帧的采样数量,sample_rate为采样频率
         int per_audio = 1000 * audio_codecpar->frame_size / audio_codecpar->sample_rate;
         av_log(NULL, AV_LOG_INFO, "one audio frame's duration is %dms\n", per_audio);
     }

接着执行下面的编译命令:

     gcc fps.c -o fps -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序:

    ./fps ../fuzhou.mp4

程序运行完毕,看到控制台输出以下日志信息:

    video_codecpar bit_rate=1218448
    video_codecpar width=1440
    video_codecpar height=810
    video_codecpar fps=25
    one video frame's duration is 40ms
    audio_codecpar bit_rate=128308
    audio_codecpar frame_size=1024
    audio_codecpar sample_rate=44100
    audio_codecpar nb_channels=2
    one audio frame's duration is 23ms

由此可见,该文件的视频帧率为25fps,音频的采样率为44100Hz,说明上述时间相关参数提取成功。

3.1.2 时间基准的设定

3.1.1节提到音频帧的持续时间主要有两种,一种是MP3音频的26.1毫秒,另一种是AAC音频的23.2毫秒。然而这两个持续时间都是近似值,不能用于代码中的精确计算,也就是说,毫秒不能当作音频帧的时间单位。同理,毫秒也不能当作视频帧的时间单位,虽然3.1.1节计算出视频帧的持续时间为40毫秒,但是该数值由25fps的帧率得来,如果帧率为24fps或者30fps,那么求得的视频帧持续时间是个除不尽的小数。

既然毫秒无法作为音视、频帧的时间单位,势必要引入新的时间单位,或者称作时间基准,简称时间基。注意到AVStream结构提供了r_frame_rate字段存放帧率信息,其实它还提供了time_base字段存放时间基信息,time_base字段属于AVRational分数结构,其中分子num为1,分母den为音视频的采样率。以音频为例,常见的采样率为44100Hz,则音频流的time_base字段里面的den就为44100,此时音频帧的时间基准等于time_base.num/time_base.den,也就是1/44100。相当于把1秒的时间划分成44100个时间片,如此一来,每个音频帧的持续时间必定是时间片的整数倍。

接下来编写FFmpeg代码,把视频文件中的视频时间基和音频时间基分别打印出来,看看FFmpeg是否按此办理。下面是打印时间基的代码片段(完整代码见chapter03/timebase.c)。

    // 找到视频流的索引
    int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (video_index >= 0) {
        AVStream *video_stream = fmt_ctx->streams[video_index];
        // 获取视频流的时间基准
        AVRational time_base = video_stream->time_base;
        av_log(NULL, AV_LOG_INFO, "video_stream time_base.num=%d\n", time_base.num);
        av_log(NULL, AV_LOG_INFO, "video_stream time_base.den=%d\n", time_base.den);
    }
    // 找到音频流的索引
    int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audio_index >= 0) {
        AVStream *audio_stream = fmt_ctx->streams[audio_index];
        // 获取音频流的时间基准
        AVRational time_base = audio_stream->time_base;
        av_log(NULL, AV_LOG_INFO, "audio_stream time_base.num=%d\n", time_base.num);
        av_log(NULL, AV_LOG_INFO, "audio_stream time_base.den=%d\n", time_base.den);
    }

接着执行下面的编译命令:

     gcc timebase.c -o timebase -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib
-lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序:

    ./timebase ../fuzhou.mp4

程序运行完毕,发现控制台输出以下日志信息:

    video_stream time_base.num=1
    video_stream time_base.den=90000  // 这里的视频时间基分母有时是90000,有时是12800
    audio_stream time_base.num=1
    audio_stream time_base.den=44100

由此可见,音频的时间基分母的确是44100,然而视频的时间基分母既不是25又不是30,却是90000或者12800,为何相差这么大呢?这是因为25fps和30fps都属于传输过程中的帧率,并非采集过程中的采样率。帧率可以在传输的时候再调整,而采样率早在开始采集画面的时候就确定了,所以视频的时间基分母使用采样率而非帧率。

为什么视频的采样率会是90000Hz而不是别的数值呢?查看RFC3551规范的第5小节,里面讲到:

All of these video encodings use an RTP timestamp frequency of 90,000 Hz, the same as the MPEG presentation time stamp frequency. This frequency yields exact integer timestamp increments for the typical 24 (HDTV), 25 (PAL), and 29.97 (NTSC) and 30 Hz (HDTV) frame rates and 50, 59.94 and 60 Hz field rates. While 90 kHz is the RECOMMENDED rate for future video encodings used within this profile,other rates MAY be used.

这段英文的意思是:视频的时间戳增量必须兼容24fps、25fps、30fps等帧率(Frame Rates,也称帧速率),还要兼容50Hz、60Hz等刷新率(Field Rates,也称场频率、场扫描速率),因此推荐将90kHz作为压缩后(解压前)视频的采样率。

当然,上述规范仅推荐90000Hz,同时注明其他速率也是可以用的。在有的视频中,时间基分母设为12800,主要考虑到12800的倒数是个有限小数,在计算时会方便一些,另外12800Hz支持16fps、25fps、32fps、50fps、64fps等帧率,在实际应用中也足够了。

3.1.3 时间戳的计算

3.1.2节介绍的时间基属于音视频的时间基,也就是度量单个音频样本或者视频样本持续时长的时间单位。有了时间基以后,才能标记每个数据包所处的时间刻度,也就是时间戳。类似于信封上的邮戳,每个邮戳标记了当前信封的投递时间,每个时间戳也标记了当前数据包的解压时间或者播放时间。

FFmpeg把数据包的解压时间称作DTS(Decompression Timestamp,解压时间戳),对应AVPacket结构的dts字段;把数据包的显示时间称作PTS(Presentation Timestamp,显示时间戳),对应AVPacket结构的pts字段。对于音频帧来说,它的DTS和PTS数值保持一致,没有区别。对于视频帧来说,它的DTS和PTS的数值很可能不一样。因为视频帧分为I帧、P帧、B帧三类,其中I帧属于关键帧,也叫作帧内编码图像(Intra-Coded Picture),它包含一幅完整的图像信息;P帧属于前向预测编码图像(Predictive-Coded Picture),它需要参考前方的I帧或者P帧;B帧属于双向预测内插编码图像(Bi-Directionally Predicted Picture),它需要同时参考前方和后方的I帧或者P帧。举个例子,图3-1是一段录像的视频帧显示序列。

图3-1 一段录像的视频帧显示序列

由图3-1可见,这段录像以关键帧I1开始,其后序号为4的P1参考了I1,序号为7的P2又参考了P1;注意序号2和序号3的两个B帧同时参考了前方的I1和后方的P1,序号5和序号6的两个B帧同时参考了前方的P1和P2。P1和P2都参考前方的I帧或者P帧,这便叫作前向预测;B1、B2、B3、B4同时参考前方和后方的I帧或者P帧,这便叫作双向预测。

不过图3-1仅描绘了这些视频帧的显示顺序,在视频帧的解码过程中,解压顺序又是另一回事。比如B1参考了前方的I1和后方的P1,那么势必等待I1和P1都解压完了,才能接着解压B1,至于B2的解压顺序同理可得。又如B3参考了前方的P1和后方的P2,就得等待P1和P2都解压完了,才能接着解压B3,至于B4的解压顺序同理可得。此时视频帧的解压顺序排列如图3-2所示。

图3-2 一段录像的视频帧解压序列

至此,得到了视频帧的两种队列,一种是解压过程中的解压队列,该队列中的各帧时间戳被称作解压时间戳(DTS);另一种是显示过程中的显示队列,该队列中的各帧时间戳被称作显示时间戳(PTS)。不过,开发者只需关注PTS,因为什么时候采用B帧由编解码器自行判断,所以DTS也由编解码器来设定。

那么PTS的数值又是怎么计算出来的呢?在日常生活中,最小的时间单位为1s(秒),则时间计数就是1s、2s、3s、4s、5s这样。既然音视频给出了某种时间基,其内部的时间计数能否采用1个时间基、2个时间基、3个时间基等表达呢?尽管这个思路没什么问题,不过音视频存在解压与播放两个操作。就视频而言,解压过程中的采样率可能是90000Hz或者12800Hz,而播放过程中的帧率可能是15fps、25fps等。因此,综合解压和播放两个过程,视频的时间戳增量等于采样率除以帧率,即视频的时间戳增量=采样率÷帧率=采样率×一帧视频持续的时间,这个增量时间戳的单位就是视频的时间基。

例如,某个视频的采样率是90000Hz,帧率是15fps,则它的时间戳增量=90000÷15=6000,于是各视频帧的PTS为0、6000、12000、18000、24000等。再如,某个视频的采样率为12800Hz,帧率为25fps,则它的时间戳增量=12800÷25=512,于是各视频帧的PTS为0、512、1024、1536、2048等。

对于音频来说,它在解压和播放两个操作中的采样率是同样的,一帧音频持续的时间等于一帧音频的采样数除以采样率,那么音频的时间戳增量等于一帧音频持续的时间乘以采样率。即音频的时间戳增量=采样率×一帧音频持续的时间=采样率×一帧音频的采样数÷采样率。鉴于采样率先除再乘刚好被抵扣掉,因此音频的时间戳增量就等于一帧音频的采样数,这个增量时间戳的单位就是音频的时间基。

以MP3标准为例,每帧MP3音频的采样个数为1152,则各音频帧的PTS为0、1152、2304、3456等。再来看AAC标准,每帧AAC音频的采样个数为1024,则各音频帧的PTS为0、1024、2048、4096等。在实际应用中,视频文件中的音频流,首个AAC音频帧的PTS可能为−1024,第二帧的PTS才变为0。这是因为首帧音频先于首帧视频播放,如果首帧视频的PTS为0,则首帧音频的PTS自然变成−1024了。只有首帧音频与首帧视频在同一时刻播放,它们的PTS才会为相同的0。

接下来编写FFmpeg代码,根据视频文件中的音视频时间基,分别计算对应的时间戳增量。计算音视频时间戳增量的代码片段如下(完整代码见chapter03/timestamp.c)。

    // 找到视频流的索引
    int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (video_index >= 0) {
        AVStream *video_stream = fmt_ctx->streams[video_index];
        // 获取视频的时间基
        AVRational time_base = video_stream->time_base;
        av_log(NULL, AV_LOG_INFO, "video_stream time_base.num=%d\n", time_base.num);
        av_log(NULL, AV_LOG_INFO, "video_stream time_base.den=%d\n", time_base.den);
        // 计算视频帧的时间戳增量
        int timestamp_increment = 1 * time_base.den / fps;
        av_log(NULL, AV_LOG_INFO, "video timestamp_increment=%d\n", timestamp_increment);
    }
    // 找到音频流的索引
    int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audio_index >= 0) {
        AVStream *audio_stream = fmt_ctx->streams[audio_index];
        // 获取音频的时间基
        AVRational time_base = audio_stream->time_base;
        av_log(NULL, AV_LOG_INFO, "audio_stream time_base.num=%d\n", time_base.num);
        av_log(NULL, AV_LOG_INFO, "audio_stream time_base.den=%d\n", time_base.den);
        // 计算音频帧的时间戳增量
        int timestamp_increment = 1 * audio_codecpar->frame_size *
                        (time_base.den / audio_codecpar->sample_rate);
        av_log(NULL, AV_LOG_INFO, "audio timestamp_increment=%d\n", timestamp_increment);
    }

接着执行下面的编译命令:

     gcc timestamp.c -o timestamp -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib
-lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序:

    ./timestamp ../fuzhou.mp4

程序运行完毕,发现控制台输出以下日志信息:

    video_stream time_base.num=1
    video_stream time_base.den=12800
    video timestamp_increment=512
    audio_stream time_base.num=1
    audio_stream time_base.den=44100
    audio timestamp_increment=1024

由此可见,该文件的视频时间戳增量为512,音频时间戳增量为1024。

在视频文件中,音频流的time_base.den一般等于audio_codecpar->sample_rate,此时音频的时间戳增量正好等于采样个数。但在音频文件中,time_base.den往往是audio_codecpar-> sample_rate的几十倍,此时音频的时间戳增量不等于采样个数,而是后者的几十倍。 Ux7Y5C1oOvezLf76wMu1ExNSFB8SA3S92pYNHo297yhmwJACu4ZPdbmI7bTbxMH2

点击中间区域
呼出菜单
上一章
目录
下一章
×