和播放音频一样,采用生产者消费者模型。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
渲染。
由于人们对声音更敏感,视频画面的一会儿快一会儿慢是察觉不出来的。而 声音的节奏变化是很容易察觉的。所以我们这里采用第一种方式来同步音视频。 这里需要计算当前视频帧的播放时间和当前音频的播放时间来进行比较,然后计算出睡眠时间来让视频不渲染还是延迟渲染,保持音视频尽量同步。
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;
}
//如果>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;
}
//延时时间 单位秒
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
的时候设置标识,清除缓冲队列,
调用
avcodec_flush_buffers(&AVCodecContext);
进行seek
,接着清空队列,并调用
avformat_seek_file(pFormatCtx, -1, INT64_MIN, rel, INT64_MAX, 0);
清空ffmpeg
的缓存。
注意
AVFormatContext
获取AvPacket
,有一个线程在使用AVCodecContext
在进行解码,需要为AVFormatContext
和AVCodecContext
添加锁。防止同步问题造成其他问题。seek
之前,我们的数据已经读取完了存储在缓冲队列里面,这里seek
清空缓冲队列,就会播放完毕,所以我们需要在读取不到数据的时候也加上seek
标识判断。比如//这里是读取数据完毕的时候
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;
}
}
这里需要特别注意的是线程退出的问题
单个线程退出
使用
return
代替pthread_exit();
多个线程退出
使用pthread_join(thread_t, NULL),会阻塞当前线程,直到thread_t退出完。退出的时候需要理清楚线程的退出顺序。