FFmpeg学习5:多线程播放视音频
2016-09-19 23:47
363 查看
在前面的学习中,视频和音频的播放是分开进行的。这主要是为了学习的方便,经过一段时间的学习,对FFmpeg的也有了一定的了解,本文就介绍了
如何使用多线程同时播放音频和视频(未实现同步),并对前面的学习的代码进行了重构,便于后面的扩展。
本文主要有以下几个方面的内容:
多线程播放视音频的整体流程
多线程队列
音频播放
视频播放
总结以及后续的计划
对于一个打开的视频文件(也就是取得其
音频播放线程。创建一个回调函数,从Audio Packet Queue中取出Packet并解码,将解码的数据发送到SDL Audio Device中进行播放
视频播放线程。
创建Video解码线程,从Video Packet Queue中取出Packet进行解码,并将解码后的数据放入到 Video Frame Queue队列缓存中。
进入到SDL Window 事件循环中,按照一定的速度从 Video Frame Queue中取出Frame,并转换为相应的格式,然后在SDL Screen上显示
其整个流程中如下图:
封装后的
主函数的主要分为三个部分:
初始化FFmpeg和SDL
创建Audio播放线程和Video播放线程
SDL事件循环,显示图像。
MediaState 主要包含了
AudioState 播放音频所需要的数据
VideoState 播放视频所需要的数据
这里主要介绍下
结构比较简单,其主要的功能是在
主要有以下几个功能:
调用
找到audio stream的index,并打开相应的
找到video stream的index,并打开相应的
部分代码如下:
进行解码然后播放。
使用标准库中的
当要访问队列中的元素时,使用
设置
入队列的方法实现如下:
注意对入队列的Packet调用
解除对packet队列的锁定。
出队的方法实现如下:
参数
取出packet后要调用
另外几个字段是用来缓存解码后的数据的,回调函数从该缓冲区中取出数据发送到音频设备。
函数
视频解码需要的数据 packet队列、stream的index以及AVCodecContext
将解码后的中间数据
FrameQueue Frame队列,存放从packet中解码得到的Frame。要刷新新的帧时,就从该队列中取出Frame,进行格式转换后render到界面上。
frame 格式转换时中间变量
displayFrame 格式转换后的fram,给fram中的数据是最终呈现到界面上的帧
SDL播放视频需要的数据
首先创建SDL窗口的一些变量,并根据相应的格式为
video的解码线程函数如下:
该函数较简单,就是不断从packet队列中取出packet,然后进行解码,将解码得到的frame队列中,供display线程使用,最终呈现到界面上。注意的是,这里给frame队列设置一个最大容量,当frame队列已满的时候,就阻塞解码线程,等待display线程播放一段时间。
在
发送一个
该函数会从frame队列中取出每一个frame,做了格式转换后呈现到界面上。
该函数的实现也挺清晰的,不断的从frame队列中取出frame,创建
其占用的内存一直在增长,不用说肯定是内存泄漏了呀。我是着重对几个缓存队列进行检测,没有发现问题。最后实在没有办法,一段一段代码的进行检查,最终发现是使用完了
从毕业到现在进公司快3个月了,基本是打酱油的三个月,公司的代码都没有看到过,整天对着电脑屏幕没有事情可做。
后面的一些计划吧,督促下自己不能这么懒散
实现视音频的同步
改用C++11的多线程库
再对代码进行下重构,可以使用不同的UI库进行渲染(打算换Qt试试)
本文的代码 FSplayer
如何使用多线程同时播放音频和视频(未实现同步),并对前面的学习的代码进行了重构,便于后面的扩展。
本文主要有以下几个方面的内容:
多线程播放视音频的整体流程
多线程队列
音频播放
视频播放
总结以及后续的计划
1. 整体流程
FFmpeg和SDL的初始化过程这里不再赘述。整个流程如下:对于一个打开的视频文件(也就是取得其
AVFormatContext),创建一个分离线程,不断的从stream中读取Packet,并按照其stream index,将Packet分别存放到Audio Packet Queue和Video Packet这两个队列缓存中。
音频播放线程。创建一个回调函数,从Audio Packet Queue中取出Packet并解码,将解码的数据发送到SDL Audio Device中进行播放
视频播放线程。
创建Video解码线程,从Video Packet Queue中取出Packet进行解码,并将解码后的数据放入到 Video Frame Queue队列缓存中。
进入到SDL Window 事件循环中,按照一定的速度从 Video Frame Queue中取出Frame,并转换为相应的格式,然后在SDL Screen上显示
其整个流程中如下图:
1.1 重构后的main函数
在前面的学习过程中,主要是跟着dranger tutorial。由于该教程是基于C语言的,在其使用多线程播放音视频的教程中,代码使用不是很方便。在本文中,使用C++对其代码进行了重构封装。封装后的
main函数如下:
av_register_all(); SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER); char* filename = "F:\\test.rmvb"; MediaState media(filename); if (media.openInput()) SDL_CreateThread(decode_thread, "", &media); // 创建解码线程,读取packet到队列中缓存 media.audio->audio_play(); // create audio thread media.video->video_play(); // create video thread SDL_Event event; while (true) // SDL event loop { SDL_WaitEvent(&event); switch (event.type) { case FF_QUIT_EVENT: case SDL_QUIT: quit = 1; SDL_Quit(); return 0; break; case FF_REFRESH_EVENT: video_refresh_timer(media.video); break; default: break; } }
主函数的主要分为三个部分:
初始化FFmpeg和SDL
创建Audio播放线程和Video播放线程
SDL事件循环,显示图像。
1.2 使用到的数据结构
将播放过程中需要使用到的主要数据封装为三个结构:MediaState 主要包含了
AudioState和
VideoState指针,以及
AVFormatContext
AudioState 播放音频所需要的数据
VideoState 播放视频所需要的数据
这里主要介绍下
MediaState,在后面播放音频和视频时再介绍与其相关的数据结构。
MediaState的声明如下:
struct MediaState { AudioState *audio; VideoState *video; AVFormatContext *pFormatCtx; char* filename; //bool quit; MediaState(char *filename); ~MediaState(); bool openInput(); };
结构比较简单,其主要的功能是在
oepnInput中,该函数用来打开相应的video文件,并读取相应的信息填充到
VideoState和
AudioState结构中。
主要有以下几个功能:
调用
avformat_open_input获取AVFormatContext的指针
找到audio stream的index,并打开相应的
AVCodecContext
找到video stream的index,并打开相应的
AVCodecContext
1.3 Packet分离线程
调用oepnInput后,以获取到足够的信息,然后创建packet分离线程,按照得到的stream index,将
av_read_frame读取到的packet分别放到相应的packet 缓存队列中。
部分代码如下:
if (packet->stream_index == media->audio->audio_stream) // audio stream { media->audio->audioq.enQueue(packet); av_packet_unref(packet); } else if (packet->stream_index == media->video->video_stream) // video stream { media->video->videoq->enQueue(packet); av_packet_unref(packet); } else av_packet_unref(packet);
2.多线程队列
分离线程将读取到的Packet分别存放到视频和音频的packet队列中,这个Packet队列会被多个线程访问,分离线程向里面填充Packet;视频和音频播放线程取出队列中的packet进行解码然后播放。
PacketQueue的声明如下:
struct PacketQueue { std::queue<AVPacket> queue; Uint32 nb_packets; Uint32 size; SDL_mutex *mutex; SDL_cond *cond; PacketQueue(); bool enQueue(const AVPacket *packet); bool deQueue(AVPacket *packet, bool block); };
使用标准库中的
std::queue作为存放数据的容器,
SDL_mutex和
SDL_cond是SDL库中提供的互斥量和条件变量用来控制队列的线程的同步。
当要访问队列中的元素时,使用
SDL_mutex来锁定队列;当队列中没有Packet时,而此时又有视频或者音频线程取队列中的Packet,就需要设置一个
设置
SDL_cond信号量等待新的Packet入队列。
入队列的方法实现如下:
bool PacketQueue::enQueue(const AVPacket *packet) { AVPacket *pkt = av_packet_alloc(); if (av_packet_ref(pkt, packet) < 0) return false; SDL_LockMutex(mutex); queue.push(*pkt); size += pkt->size; nb_packets++; SDL_CondSignal(cond); SDL_UnlockMutex(mutex); return true; }
注意对入队列的Packet调用
av_packet_ref增加引用计数的方法来复制Packet中的数据。在将新的packet入队以后,设置信号量通知有新的packet入队列,并
解除对packet队列的锁定。
出队的方法实现如下:
bool PacketQueue::deQueue(AVPacket *packet, bool block) { bool ret = false; SDL_LockMutex(mutex); while (true) { if (quit) { ret = false; break; } if (!queue.empty()) { if (av_packet_ref(packet, &queue.front()) < 0) { ret = false; break; } //av_packet_free(&queue.front()); AVPacket pkt = queue.front(); queue.pop(); av_packet_unref(&pkt); nb_packets--; size -= packet->size; ret = true; break; } else if (!block) { ret = false; break; } else { SDL_CondWait(cond, mutex); } } SDL_UnlockMutex(mutex); return ret; }
参数
block标识在队列为空的时候是否阻塞等待,当设置为true的时候,取packet的线程会阻塞等待,直到得到cond信号量的通知。另外,在
取出packet后要调用
av_packet_unref减少packet数据的引用计数。
3. 音频播放
音频的播放在前面已经做个总结FFmpeg学习3:播放音频,其播放过程主要是设置好向音频设备发送数据的回调函数,这里就不再详述。和以前不同的是对播放数据进行了封装,如下:struct AudioState { const uint32_t BUFFER_SIZE;// 缓冲区的大小 PacketQueue audioq; uint8_t *audio_buff; // 解码后数据的缓冲空间 uint32_t audio_buff_size; // buffer中的字节数 uint32_t audio_buff_index; // buffer中未发送数据的index int audio_stream; // audio流index AVCodecContext *audio_ctx; // 已经调用avcodec_open2打开 AudioState(); //默认构造函数 AudioState(AVCodecContext *audio_ctx, int audio_stream); ~AudioState(); /** * audio play */ bool audio_play(); };
audioq是存放audio packet的队列;
audio_stream是audio stream的index
另外几个字段是用来缓存解码后的数据的,回调函数从该缓冲区中取出数据发送到音频设备。
audio_buff缓冲区的指针
audio_buff_size缓冲区中数据的多少
audio_buff_index缓冲区中已经发送数据的指针
BUFFER_SIZE缓冲区的最大容量
函数
audio_play用来设置播放所需的参数,并启动音频播放线程
bool AudioState::audio_play() { SDL_AudioSpec desired; desired.freq = audio_ctx->sample_rate; desired.channels = audio_ctx->channels; desired.format = AUDIO_S16SYS; desired.samples = 1024; desired.silence = 0; desired.userdata = this; desired.callback = audio_callback; if (SDL_OpenAudio(&desired, nullptr) < 0) { return false; } SDL_PauseAudio(0); // playing return true; }
4. 视频播放
4.1 VideoState
和音频播放类似,也封装了一个VideoState保存视频播放时所需的数据
struct VideoState { PacketQueue* videoq; // 保存的video packet的队列缓存 int video_stream; // index of video stream AVCodecContext *video_ctx; // have already be opened by avcodec_open2 FrameQueue frameq; // 保存解码后的原始帧数据 AVFrame *frame; AVFrame *displayFrame; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *bmp; SDL_Rect rect; void video_play(); VideoState(); ~VideoState(); };
VideoState中的字段大体上可以分为三类:
视频解码需要的数据 packet队列、stream的index以及AVCodecContext
将解码后的中间数据
FrameQueue Frame队列,存放从packet中解码得到的Frame。要刷新新的帧时,就从该队列中取出Frame,进行格式转换后render到界面上。
frame 格式转换时中间变量
displayFrame 格式转换后的fram,给fram中的数据是最终呈现到界面上的帧
SDL播放视频需要的数据
FrameQueue的实现和
PacketQueue的实现类似,不再赘述。
4.2 Video的decode和play
在VideoState中函数
video_play用来进行video播放的初始化工作,并开启video的解码线程
void VideoState::video_play() { int width = 800; int height = 600; // 创建sdl窗口 window = SDL_CreateWindow("FFmpeg Decode", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_OPENGL); renderer = SDL_CreateRenderer(window, -1, 0); bmp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, width, height); rect.x = 0; rect.y = 0; rect.w = width; rect.h = height; frame = av_frame_alloc(); displayFrame = av_frame_alloc(); displayFrame->format = AV_PIX_FMT_YUV420P; displayFrame->width = width; displayFrame->height = height; int numBytes = avpicture_get_size((AVPixelFormat)displayFrame->format,displayFrame->width, displayFrame->height); uint8_t *buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t)); avpicture_fill((AVPicture*)displayFrame, buffer, (AVPixelFormat)displayFrame->format, displayFrame->width, displayFrame->height); SDL_CreateThread(decode, "", this); schedule_refresh(this, 40); // start display }
首先创建SDL窗口的一些变量,并根据相应的格式为
displayFrame分配数据空间;接着创建video的解码线程;最后一句
schedule_refresh(this, 40)是开始SDL的事件循环,并在窗口上不断的刷新帧。
video的解码线程函数如下:
int decode(void *arg) { VideoState *video = (VideoState*)arg; AVFrame *frame = av_frame_alloc(); AVPacket packet; while (true) { video->videoq->deQueue(&packet, true); int ret = avcodec_send_packet(video->video_ctx, &packet); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) continue; ret = avcodec_receive_frame(video->video_ctx, frame); if (ret < 0 && ret != AVERROR_EOF) continue; if (video->frameq.nb_frames >= FrameQueue::capacity) SDL_Delay(500); video->frameq.enQueue(frame); av_frame_unref(frame); } av_frame_free(&frame); return 0; }
该函数较简单,就是不断从packet队列中取出packet,然后进行解码,将解码得到的frame队列中,供display线程使用,最终呈现到界面上。注意的是,这里给frame队列设置一个最大容量,当frame队列已满的时候,就阻塞解码线程,等待display线程播放一段时间。
4.3 display线程
帧的呈现借助了SDL库,所以display线程实际就是SDL的窗口时间循环。视频帧的显示过程如下图:在
video_play函数中,启动视频的解码线程后,就调用了
schedule_refresh函数来开始帧的显示线程。
// 延迟delay ms后刷新video帧 void schedule_refresh(VideoState *video, int delay) { SDL_AddTimer(delay, sdl_refresh_timer_cb, video); } uint32_t sdl_refresh_timer_cb(uint32_t interval, void *opaque) { SDL_Event event; event.type = FF_REFRESH_EVENT; event.user.data1 = opaque; SDL_PushEvent(&event); return 0; /* 0 means stop timer */ }
schedule_refresh设置一个延迟时间,然后调用
sdl_refresh_timer_cb函数。
sdl_refresh_timer_cb是向SDL的事件循环
发送一个
FF_REFRESH_EVENT事件。从前面的事件处理中可知,在接收到
FF_REFRESH_EVENT事件后,会调用
video_refresh_timer
该函数会从frame队列中取出每一个frame,做了格式转换后呈现到界面上。
void video_refresh_timer(void *userdata) { VideoState *video = (VideoState*)userdata; if (video->video_stream >= 0) { if (video->videoq->queue.empty()) schedule_refresh(video, 1); else { /* Now, normally here goes a ton of code about timing, etc. we're just going to guess at a delay for now. You can increase and decrease this value and hard code the timing - but I don't suggest that ;) We'll learn how to do it for real later. */ schedule_refresh(video, 40); video->frameq.deQueue(&video->frame); SwsContext *sws_ctx = sws_getContext(video->video_ctx->width, video->video_ctx->height, video->video_ctx->pix_fmt, video->displayFrame->width,video->displayFrame->height,(AVPixelFormat)video->displayFrame->format, SWS_BILINEAR, nullptr, nullptr, nullptr); sws_scale(sws_ctx, (uint8_t const * const *)video->frame->data, video->frame->linesize, 0, video->video_ctx->height, video->displayFrame->data, video->displayFrame->linesize); // Display the image to screen SDL_UpdateTexture(video->bmp, &(video->rect), video->displayFrame->data[0], video->displayFrame->linesize[0]); SDL_RenderClear(video->renderer); SDL_RenderCopy(video->renderer, video->bmp, &video->rect, &video->rect); SDL_RenderPresent(video->renderer); sws_freeContext(sws_ctx); av_frame_unref(video->frame); } } else { schedule_refresh(video, 100); } }
该函数的实现也挺清晰的,不断的从frame队列中取出frame,创建
SwsContext按照
VideoState中设置的参数对frame进行格式转换。这里要提一个血泪教训,在使用完
SwsContext后一定要记得调用
sws_freeContext释放。在写好本文的demo后,播放视频的发现
其占用的内存一直在增长,不用说肯定是内存泄漏了呀。我是着重对几个缓存队列进行检测,没有发现问题。最后实在没有办法,一段一段代码的进行检查,最终发现是使用完了
SwsContext没有释放掉。起初时候,我就认为
SwsContext只是设置一个转换参数,也没在意,谁知道会占用那么大的空间,播放一个视频内存的占用一度达到一个G,这只是播放了十几分钟。
Summary
从上一篇总结到现在,磨蹭了将近半个月终于算是把这个多线程播放弄完了,从中真是学到了不少东西。从毕业到现在进公司快3个月了,基本是打酱油的三个月,公司的代码都没有看到过,整天对着电脑屏幕没有事情可做。
后面的一些计划吧,督促下自己不能这么懒散
实现视音频的同步
改用C++11的多线程库
再对代码进行下重构,可以使用不同的UI库进行渲染(打算换Qt试试)
本文的代码 FSplayer
相关文章推荐
- FFmpeg学习5:多线程播放视音频
- FFmpeg学习5:多线程播放视音频
- FFmpeg学习3:播放音频
- FFmpeg学习3:播放音频
- ffmpeg学习九:音频编码前奏-ubuntu下录音和播放
- FFmpeg学习3:播放音频
- 多线程播放视音频ffmpeg+SDL
- ffmpeg学习:ffmpeg下载,播放音频、视频,读取USB摄像头数据流并实时播放
- FFmpeg学习3:播放音频
- HTML5 学习之音频与视频的播放
- 学习wpf播放视频音频的两种不同方法
- Android学习篇章51-MediaPlayer-音频播放
- FFmpeg:播放音频
- FFMPEG + SDL音频播放分析
- FFMPEG视音频编解码入门学习总结
- FFMPEG + SDL音频播放分析
- Android应用开发学习笔记之播放音频
- 由播放音频后不能录音的原因-学习AudioSession设置
- FFMPEG + SDL音频播放分析
- OpenAL学习笔记(一)---播放音频(*.wav)