目录
视频播放器基本原理
解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。解封装
将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。解码
将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如 PCM 数据。音视频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。
简易播放器的实现——音视频播放
音视频同步的目的是为了使播放的声音和显示的画面保持一致。视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。
我们以一个44.1KHz的AAC音频流和25FPS的H264视频流为例,来看一下理想情况下音视频的同步过程:
一个AAC音频frame每个声道包含1024个采样点(也可能是2048,参“FFmpeg关于nb_smples,frame_size以及profile的解释”),则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个H264视频frame播放时长(duration)为:1000ms/25 = 40ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,音视频播放过程如下图所示:
音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
程序结构
作者基于eclipse来创建C++工程 ,目录结构如下:
程序源码
audio.cpp
#include "player.h"
#include "packet.h"
#include "frame.h"
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len);
// 从packet_queue中取一个packet,解码生成frame
static int audio_decode_frame(AVCodecContext *p_codec_ctx, packet_queue_t *p_pkt_queue, AVFrame *frame)
{
int ret;
while (1)
{
AVPacket pkt;
while (1)
{
//if (d->queue->abort_request)
// return -1;
// 3.2 一个音频packet含一至多个音频frame,每次avcodec_receive_frame()返回一个frame,此函数返回。
// 下次进来此函数,继续获取一个frame,直到avcodec_receive_frame()返回AVERROR(EAGAIN),
// 表示解码器需要填入新的音频packet
ret = avcodec_receive_frame(p_codec_ctx, frame);
if (ret >= 0)
{
// 时基转换,从d->avctx->pkt_timebase时基转换到1/frame->sample_rate时基
AVRational tb = (AVRational) { 1, frame->sample_rate };
if (frame->pts != AV_NOPTS_VALUE)
{
frame->pts = av_rescale_q(frame->pts, p_codec_ctx->pkt_timebase, tb);
}
else
{
av_log(NULL, AV_LOG_WARNING, "frame->pts no\n");
}
return 1;
}
else if (ret == AVERROR_EOF)
{
av_log(NULL, AV_LOG_INFO, "audio avcodec_receive_frame(): the decoder has been flushed\n");
avcodec_flush_buffers(p_codec_ctx);
return 0;
}
else if (ret == AVERROR(EAGAIN))
{
// av_log(NULL, AV_LOG_INFO, "audio avcodec_receive_frame(): input is not accepted in the current state\n");
break;
}
else
{
av_log(NULL, AV_LOG_ERROR, "audio avcodec_receive_frame(): other errors\n");
continue;
}
}
// 1. 取出一个packet。使用pkt对应的serial赋值给d->pkt_serial
if (packet_queue_get(p_pkt_queue, &pkt, true) < 0)
{
return -1;
}
// packet_queue中第一个总是flush_pkt。每次seek操作会插入flush_pkt,更新serial,开启新的播放序列
if (pkt.data == NULL)
{
// 复位解码器内部状态/刷新内部缓冲区。当seek操作或切换流时应调用此函数。
avcodec_flush_buffers(p_codec_ctx);
}
else
{
// 2. 将packet发送给解码器
// 发送packet的顺序是按dts递增的顺序,如IPBBPBB
// pkt.pos变量可以标识当前packet在视频文件中的地址偏移
if (avcodec_send_packet(p_codec_ctx, &pkt) == AVERROR(EAGAIN))
{
av_log(NULL, AV_LOG_ERROR, "receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
}
av_packet_unref(&pkt);
}
}
}
// 音频解码线程:从音频packet_queue中取数据,解码后放入音频frame_queue
static int audio_decode_thread(void *arg)
{
player_stat_t *is = (player_stat_t *)arg;
AVFrame *p_frame = av_frame_alloc();
frame_t *af;
int got_frame = 0;
AVRational tb;
int ret = 0;
if (p_frame == NULL)
{
return AVERROR(ENOMEM);
}
while (1)
{
got_frame = audio_decode_frame(is->p_acodec_ctx, &is->audio_pkt_queue, p_frame);
if (got_frame < 0)
{
goto the_end;
}
if (got_frame)
{
tb = (AVRational) { 1, p_frame->sample_rate };
if (!(af = frame_queue_peek_writable(&is->audio_frm_queue)))
goto the_end;
af->pts = (p_frame->pts == AV_NOPTS_VALUE) ? NAN : p_frame->pts * av_q2d(tb);
af->pos = p_frame->pkt_pos;
//-af->serial = is->auddec.pkt_serial;
// 当前帧包含的(单个声道)采样数/采样率就是当前帧的播放时长
af->duration = av_q2d((AVRational) { p_frame->nb_samples, p_frame->sample_rate });
// 将frame数据拷入af->frame,af->frame指向音频frame队列尾部
av_frame_move_ref(af->frame, p_frame);
// 更新音频frame队列大小及写指针
frame_queue_push(&is->audio_frm_queue);
}
}
the_end:
av_frame_free(&p_frame);
return ret;
}
int open_audio_stream(player_stat_t *is)
{
AVCodecContext *p_codec_ctx;
AVCodecParameters *p_codec_par = NULL;
AVCodec* p_codec = NULL;
int ret;
// 1. 为音频流构建解码器AVCodecContext
// 1.1 获取解码器参数AVCodecParameters
p_codec_par = is->p_audio_stream->codecpar;
// 1.2 获取解码器
p_codec = avcodec_find_decoder(p_codec_par->codec_id);
if (p_codec == NULL)
{
av_log(NULL, AV_LOG_ERROR, "Cann't find codec!\n");
return -1;
}
// 1.3 构建解码器AVCodecContext
// 1.3.1 p_codec_ctx初始化:分配结构体,使用p_codec初始化相应成员为默认值
p_codec_ctx = avcodec_alloc_context3(p_codec);
if (p_codec_ctx == NULL)
{
av_log(NULL, AV_LOG_ERROR, "avcodec_alloc_context3() failed\n");
return -1;
}
// 1.3.2 p_codec_ctx初始化:p_codec_par ==> p_codec_ctx,初始化相应成员
ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "avcodec_parameters_to_context() failed %d\n", ret);
return -1;
}
// 1.3.3 p_codec_ctx初始化:使用p_codec初始化p_codec_ctx,初始化完成
ret = avcodec_open2(p_codec_ctx, p_codec, NULL);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "avcodec_open2() failed %d\n", ret);
return -1;
}
p_codec_ctx->pkt_timebase = is->p_audio_stream->time_base;
is->p_acodec_ctx = p_codec_ctx;
// 2. 创建视频解码线程
SDL_CreateThread(audio_decode_thread, "audio decode thread", is);
return 0;
}
static int audio_resample(player_stat_t *is, int64_t audio_callback_time)
{
int data_size, resampled_data_size;
int64_t dec_channel_layout;
av_unused double audio_clock0;
int wanted_nb_samples;
frame_t *af;
if (is->paused)
{
return -1;
}
#if defined(_WIN32)
while (frame_queue_nb_remaining(&is->audio_frm_queue) == 0)
{
if ((av_gettime_relative() - audio_callback_time) > 1000000LL * is->audio_hw_buf_size / is->audio_param_tgt.bytes_per_sec / 2)
return -1;
av_usleep(1000);
}
#endif
// 若队列头部可读,则由af指向可读帧
if (!(af = frame_queue_peek_readable(&is->audio_frm_queue)))
return -1;
frame_queue_next(&is->audio_frm_queue);
// 根据frame中指定的音频参数获取缓冲区的大小
data_size = av_samples_get_buffer_size(NULL, af->frame->channels, // 本行两参数:linesize,声道数
af->frame->nb_samples, // 本行一参数:本帧中包含的单个声道中的样本数
(AVSampleFormat) af->frame->format, 1); // 本行两参数:采样格式,不对齐
// 获取声道布局
dec_channel_layout =
(af->frame->channel_layout && af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ?
af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);
wanted_nb_samples = af->frame->nb_samples;
// is->audio_param_tgt是SDL可接受的音频帧数,是audio_open()中取得的参数
// 在audio_open()函数中又有“is->audio_src = is->audio_param_tgt”
// 此处表示:如果frame中的音频参数 == is->audio_src == is->audio_param_tgt,那音频重采样的过程就免了(因此时is->swr_ctr是NULL)
// 否则使用frame(源)和is->audio_param_tgt(目标)中的音频参数来设置is->swr_ctx,并使用frame中的音频参数来赋值is->audio_src
if (af->frame->format != is->audio_param_src.fmt ||
dec_channel_layout != is->audio_param_src.channel_layout ||
af->frame->sample_rate != is->audio_param_src.freq)
{
swr_free(&is->audio_swr_ctx);
// 使用frame(源)和is->audio_param_tgt(目标)中的音频参数来设置is->audio_swr_ctx
is->audio_swr_ctx = swr_alloc_set_opts(NULL,
is->audio_param_tgt.channel_layout, is->audio_param_tgt.fmt, is->audio_param_tgt.freq,
dec_channel_layout, (AVSampleFormat) af->frame->format,
af->frame->sample_rate,
0, NULL);
if (!is->audio_swr_ctx || swr_init(is->audio_swr_ctx) < 0)
{
av_log(NULL, AV_LOG_ERROR,
"Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
af->frame->sample_rate, av_get_sample_fmt_name((AVSampleFormat) af->frame->format),
af->frame->channels,
is->audio_param_tgt.freq, av_get_sample_fmt_name(is->audio_param_tgt.fmt), is->audio_param_tgt.channels);
swr_free(&is->audio_swr_ctx);
return -1;
}
// 使用frame中的参数更新is->audio_src,第一次更新后后面基本不用执行此if分支了,因为一个音频流中各frame通用参数一样
is->audio_param_src.channel_layout = dec_channel_layout;
is->audio_param_src.channels = af->frame->channels;
is->audio_param_src.freq = af->frame->sample_rate;
is->audio_param_src.fmt = (AVSampleFormat) af->frame->format;
}
if (is->audio_swr_ctx)
{
// 重采样输入参数1:输入音频样本数是af->frame->nb_samples
// 重采样输入参数2:输入音频缓冲区
const uint8_t **in = (const uint8_t **)af->frame->extended_data;
// 重采样输出参数1:输出音频缓冲区尺寸
// 重采样输出参数2:输出音频缓冲区
uint8_t **out = &is->audio_frm_rwr;
// 重采样输出参数:输出音频样本数(多加了256个样本)
int out_count = (int64_t)wanted_nb_samples * is->audio_param_tgt.freq / af->frame->sample_rate + 256;
// 重采样输出参数:输出音频缓冲区尺寸(以字节为单位)
int out_size = av_samples_get_buffer_size(NULL, is->audio_param_tgt.channels, out_count, is->audio_param_tgt.fmt, 0);
int len2;
if (out_size < 0)
{
av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
return -1;
}
av_fast_malloc(&is->audio_frm_rwr, &is->audio_frm_rwr_size, out_size);
if (!is->audio_frm_rwr)
return AVERROR(ENOMEM);
// 音频重采样:返回值是重采样后得到的音频数据中单个声道的样本数
len2 = swr_convert(is->audio_swr_ctx, out, out_count, in, af->frame->nb_samples);
if (len2 < 0)
{
av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
return -1;
}
if (len2 == out_count)
{
av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small\n");
if (swr_init(is->audio_swr_ctx) < 0)
swr_free(&is->audio_swr_ctx);
}
is->p_audio_frm = is->audio_frm_rwr;
// 重采样返回的一帧音频数据大小(以字节为单位)
resampled_data_size = len2 * is->audio_param_tgt.channels * av_get_bytes_per_sample(is->audio_param_tgt.fmt);
// printf("%s:%d, resampled_data_size = %d\n", __FUNCTION__, __LINE__, resampled_data_size);
}
else
{
// 未经重采样,则将指针指向frame中的音频数据
is->p_audio_frm = af->frame->data[0];
resampled_data_size = data_size;
}
audio_clock0 = is->audio_clock;
/* update the audio clock with the pts */
if (!isnan(af->pts))
{
is->audio_clock = af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;
}
else
{
is->audio_clock = NAN;
}
is->audio_clock_serial = af->serial;
#ifdef DEBUG
{
static double last_clock;
printf("audio: delay=%0.3f clock=%0.3f clock0=%0.3f\n",
is->audio_clock - last_clock,
is->audio_clock, audio_clock0);
last_clock = is->audio_clock;
}
#endif
return resampled_data_size;
}
static int open_audio_playing(void *arg)
{
player_stat_t *is = (player_stat_t *)arg;
SDL_AudioSpec wanted_spec;
SDL_AudioSpec actual_spec;
static SDL_AudioDeviceID audio_dev;
/