ffmpeg视频播放器相关

视频播放思路

和播放音频一样,采用生产者消费者模型。AvPacket入队,然后AvPacket出队伍解码。

视频解码渲染

软解码:如果解码之后的数据格式是AV_PIX_FMT_YUV420P直接使用采用OpenGLES渲染,如果不是AV_PIX_FMT_YUV420P采用sws_scale转为AV_PIX_FMT_YUV420P在采用OpenGLES渲染。将YUV数据转换RGB的操作放在OpenGLES里面,使用GPU提升效率。软解码容易造成容易造成音视频不同步。
硬解码:在解码之前判断是否支持硬解码,如果支持硬解码就直接通过ffmpeg处理视频数据H264 H265等,为其加上头信息,然后硬解码交其OpenGLES渲染。

音视频同步问题
  1. 音频线性播放,视频同步到音频上。
  2. 视频线性播放,音频同步到视频上。
  3. 用一个外部线性时间,音频和视频都同步到这个外部时间上。

由于人们对声音更敏感,视频画面的一会儿快一会儿慢是察觉不出来的。而
声音的节奏变化是很容易察觉的。所以我们这里采用第一种方式来同步音视频。
这里需要计算当前视频帧的播放时间和当前音频的播放时间来进行比较,然后计算出睡眠时间来让视频不渲染还是延迟渲染,保持音视频尽量同步。

音视频同步相关计算
  • 计算当前视频帧播放的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double clock = 0;

if(pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
time_base = pFormatCtx->streams[i]->time_base;
}

//...

double pts = av_frame_get_best_effort_timestamp(avFrame);
if(pts == AV_NOPTS_VALUE)
{
pts = 0;
}
pts *= av_q2d(time_base);

if(pts > 0)
{
clock = pts;
}
  • 计算音视频播放时间差值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//如果>0表示音频播放在前,视频渲染慢了,需要加速渲染 <0表示音频播放在后,视频渲染快了,需要延迟渲染
double getFrameDiffTime(AVFrame *avFrame) {
double pts = av_frame_get_best_effort_timestamp(avFrame);
if(pts == AV_NOPTS_VALUE)
{
pts = 0;
}
pts *= av_q2d(time_base);

if(pts > 0)
{
clock = pts;
}

double diff = audio->clock - clock;
return diff;
}
  • 计算渲染睡眠时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 //延时时间 单位秒
double delayTime = 0;
//默认的延时时间 通过当前帧的AVRational计算fps所得 单位秒
double defaultDelayTime = 0.04;

if(pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
int num = pFormatCtx->streams[i]->avg_frame_rate.num;
int den = pFormatCtx->streams[i]->avg_frame_rate.den;
if(num != 0 && den != 0)
{
int fps = num / den;//[25 / 1]
defaultDelayTime = 1.0 / fps;
}
}

double getDelayTime(double diff) {
//如果音频的播放时间超过了30ms 视频需要加速渲染 慢慢的缩小睡眠时间 达到平缓的效果
if (diff > 0.003) {
delayTime = delayTime * 2 / 3;

if (delayTime < defaultDelayTime / 2) {
delayTime = defaultDelayTime * 2 / 3;
} else if (delayTime > defaultDelayTime * 2) {
delayTime = defaultDelayTime * 2;
}
} else if (diff < -0.003) { //如果音频的播放时间慢了30ms 视频需要延迟渲染
delayTime = delayTime * 3 / 2;
if (delayTime < defaultDelayTime / 2) {
delayTime = defaultDelayTime * 2 / 3;
} else if (delayTime > defaultDelayTime * 2) {
delayTime = defaultDelayTime * 2;
}
} else if (diff == 0.003) {

}
if (diff >= 0.5) {
delayTime = 0;
} else if (diff <= -0.5) {
delayTime = defaultDelayTime * 2;
}

if (fabs(diff) >= 10) {
delayTime = defaultDelayTime;
}
return delayTime;
}
播放暂停,停止,继续播放

解码渲染之前用一个标识判断即可

seek

和音频播放类似,解码之前采用标识判断,当调用seek的时候设置标识,清除缓冲队列,
调用

1
avcodec_flush_buffers(&AVCodecContext);

进行seek,接着清空队列,并调用

1
avformat_seek_file(pFormatCtx, -1, INT64_MIN, rel, INT64_MAX, 0);

清空ffmpeg的缓存。

注意

  1. 这里有一个线程在使用AVFormatContext获取AvPacket,有一个线程在使用AVCodecContext在进行解码,需要为AVFormatContextAVCodecContext添加锁。防止同步问题造成其他问题。
  2. 可能在seek之前,我们的数据已经读取完了存储在缓冲队列里面,这里seek清空缓冲队列,就会播放完毕,所以我们需要在读取不到数据的时候也加上seek标识判断。比如
1
2
3
4
5
6
7
8
9
10
11
12
13
//这里是读取数据完毕的时候
while(playstatus != NULL && !playstatus->exit){
if(audio->queue->getQueueSize() > 0){
av_usleep(1000 * 100);
continue;
} else{
if(!playstatus->seek){
av_usleep(1000 * 100);
playstatus->exit = true;
}
break;
}
}
release内存回收

这里需要特别注意的是线程退出的问题

单个线程退出

使用return 代替 pthread_exit();

多个线程退出

使用pthread_join(thread_t, NULL),会阻塞当前线程,直到thread_t退出完。退出的时候需要理清楚线程的退出顺序。

-------------The End-------------