前面我们已经为播放器加上了简单音视频同步功能。
播放mp4文件的时候似乎没啥问题,但是当播放rmvb文件的时候,问题就暴露出来了。
以电影天堂下载的电影文件为例:
下载地址:
CD1
ftp://dygod2:dygod2@d204.dygod.cn:2088/黑客帝国3.[中英双字.1024高分辨率]/[电影天堂www.dygod.cn]黑客帝国3CD1.rmvb |
CD2
ftp://dygod2:dygod2@d204.dygod.cn:2088/黑客帝国3.[中英双字.1024高分辨率]/[电影天堂www.dygod.cn]黑客帝国3CD2.rmvb |
CD3
ftp://dygod2:dygod2@d204.dygod.cn:2088/黑客帝国3.[中英双字.1024高分辨率]/[电影天堂www.dygod.cn]黑客帝国3CD3.rmvb |
播放第一个文件(既cd1)的时候,问题似乎不大。但播放第二个和第三个文件的时候发现音频和视频会存在固定的偏差(我测试的时候大概是2S)。
使用普通播放器打开这个文件,也会发现开始播放的前几秒,只出声音,却不出图像。
于是分析了一下CD2和CD3的文件,发现其视频pts不是从0开始的,也就是说前面一段数据里面只有音频数据,而没有视频数据。而我们实现同步的方法是通过读取的时候判断pts来决定显示不显示,因此碰到这种情况问题就出现了。
前面使用SDL的时候,我们发现,音频不管读的多块,SDL播放的时候都是按照正常的速度放出来的,因此可以在SDL的回调函数中解码出每一帧音频数据的时候,记录下音频的pts,然后根据此Pts来决定是否显示视频。
前面已经说到视频的pts不是从0开始的,且音频和视频的pts似乎不是连续的。
因此我们也要给视频开一个缓存,先读取出一定数量的视频帧,存入队列。
然后再开一个线程专门用来从视频队列中取出视频并解码,解码之后便将得到的pts与音频线程中记录的音频pts做对比,这样就可以达到同步的目的了。
那么现在 我们就有了3个线程:
1.读取视频的线程
2.解码视频的线程
3.解码播放音频的线程(这个是由SDL创建的)
1.读取视频线程如下:
读取线程和前面的差别不大,都是打开视频,然后在while循环里面读取视频,分别放入音视频的队列:
while (1) { if (av_read_frame(pFormatCtx, packet) < 0) { break; //这里认为视频读取完了 } if (packet->stream_index == videoStream) { packet_queue_put(&is->videoq, packet); //这里我们将数据存入队列 因此不调用 av_free_packet 释放 } else if( packet->stream_index == audioStream ) { packet_queue_put(&is->audioq, packet); //这里我们将数据存入队列 因此不调用 av_free_packet 释放 } else { // Free the packet that was allocated by av_read_frame av_free_packet(packet); } }
这里我们是瞬间就把整个视频读入内存了,这样我们播放小视频的话 问题不大,但是当播放大视频的时候,就把内存吃光了,因此必须要管管他。
目前想到比较好的方法就是 读取的时候限制队列的长度,当里面的数据超过某个范围的时候 就先等等。
于是改成如下:
while (1) { //这里做了个限制 当队列里面的数据超过某个大小的时候 就暂停读取 防止一下子就把视频读完了,导致的空间分配不足 /* 这里audioq.size是指队列中的所有数据包带的音频数据的总量或者视频数据总量,并不是包的数量 */ //这个值可以稍微写大一些 if (is->audioq.size > MAX_AUDIO_SIZE || is->videoq.size > MAX_VIDEO_SIZE) { SDL_Delay(10); continue; } if (av_read_frame(pFormatCtx, packet) < 0) { break; //这里认为视频读取完了 } if (packet->stream_index == videoStream) { packet_queue_put(&is->videoq, packet); //这里我们将数据存入队列 因此不调用 av_free_packet 释放 } else if( packet->stream_index == audioStream ) { packet_queue_put(&is->audioq, packet); //这里我们将数据存入队列 因此不调用 av_free_packet 释放 } else { // Free the packet that was allocated by av_read_frame av_free_packet(packet); } }
2.视频线程如下:
首先需要先创建一个线程,这里我们不再用Qt的线程 而是直接使用SDL的API来创建线程,这么做当然是为了以后移植着想。
///创建一个线程专门用来解码视频 is->video_tid = SDL_CreateThread(video_thread, "video_thread", &mVideoState);
视频线程操作如下:
int video_thread(void *arg){ VideoState *is = (VideoState *) arg; AVPacket pkt1, *packet = &pkt1; int ret, got_picture, numBytes; double video_pts = 0; //当前视频的pts double audio_pts = 0; //音频pts ///解码视频相关 AVFrame *pFrame, *pFrameRGB; uint8_t *out_buffer_rgb; //解码后的rgb数据 struct SwsContext *img_convert_ctx; //用于解码后的视频格式转换 AVCodecContext *pCodecCtx = is->video_st->codec; //视频解码器 pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); ///这里我们改成了 将解码后的YUV数据转换成RGB32 img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); numBytes = avpicture_get_size(PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height); out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); avpicture_fill((AVPicture *) pFrameRGB, out_buffer_rgb, PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height); while(1) { if (packet_queue_get(&is->videoq, packet, 1) <= 0) break;//队列里面没有数据了 读取完毕了 ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); // if (ret < 0) { // printf("decode error. "); // return; // } if (packet->dts == AV_NOPTS_VALUE && pFrame->opaque&& *(uint64_t*) pFrame->opaque != AV_NOPTS_VALUE) { video_pts = *(uint64_t *) pFrame->opaque; } else if (packet->dts != AV_NOPTS_VALUE) { video_pts = packet->dts; } else { video_pts = 0; } video_pts *= av_q2d(is->video_st->time_base); video_pts = synchronize_video(is, pFrame, video_pts); while(1) { audio_pts = is->audio_clock; if (video_pts <= audio_pts) break; int delayTime = (video_pts - audio_pts) * 1000; delayTime = delayTime > 5 ? 5:delayTime; SDL_Delay(delayTime); } if (got_picture) { sws_scale(img_convert_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); //把这个RGB数据 用QImage加载 QImage tmpImg((uchar *)out_buffer_rgb,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示 is->player->disPlayVideo(image); //调用激发信号的函数 } av_free_packet(packet); } av_free(pFrame); av_free(pFrameRGB); av_free(out_buffer_rgb); return 0; }
3.音频线程
至于音频线程就不说了,我也是直接拷贝了被人的代码。其实主要就是在前面的基础上加了获取音频pts的功能,至于怎么获取没有仔细研究他。
主要参考了这篇博客:http://blog.csdn.net/tanlon_0308/article/details/40428139
到这里,我们的播放器已经实现了真正意义上的同步,这次是真的可以播放所有的视频了。
当然啦,这还不能叫播放器,连基本的播放暂停都没有,后面我们就给他加上播放、暂停、跳转等常用的功能。
完整工程下载地址:
http://download.csdn.net/detail/qq214517703/9630369
======BUG修复 Begin =======
2017-11-28更新:
此程序在Qt5下使用的时候会有一个bug,既播放器后打开后是黑屏的,原因是文件没有正常打开,当时用的是Qt4测试的,所有木有发现。
解决方法其实很简单:
将void VideoPlayer::run()里面的
char *file_path = mFileName.toUtf8().data();
修改成如下即可:
char file_path[512] = {0}; strcpy(file_path, mFileName.toUtf8().data())
======BUG修复 End =======
学习音视频技术欢迎访问 http://blog.yundiantech.com
音视频技术交流讨论欢迎加 QQ群 121376426