在前面的学习中,视频和音频的播放是分开进行的。这主要是为了学习的方便,经过一段时间的学习,对FFmpeg的也有了一定的了解,本文就介绍了

如何使用多线程同时播放音频和视频(未实现同步),并对前面的学习的代码进行了重构,便于后面的扩展。

本文主要有以下几个方面的内容:

  • 多线程播放视音频的整体流程
  • 多线程队列
  • 音频播放
  • 视频播放
  • 总结以及后续的计划

1. 整体流程

FFmpeg和SDL的初始化过程这里不再赘述。整个流程如下:

  • 对于一个打开的视频文件(也就是取得其AVFormatContext),创建一个分离线程,不断的从stream中读取Packet,并按照其stream index,将Packet分别存放到Audio Packet QueueVideo 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 主要包含了AudioStateVideoState指针,以及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文件,并读取相应的信息填充到VideoStateAudioState结构中。

主要有以下几个功能:

  • 调用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_mutexSDL_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:多线程播放视音频的更多相关文章

  1. ffmpeg学习笔记-多线程音视频解码

    之前的视频解码仍然存在问题,那就是是在主线程中去完成解码的,会造成线程阻塞,这里将其改为多线程解码,使其主线程不被阻塞 前面介绍了音视频的主线程解码,那样会阻塞主线程,在前面学习了多线程以后,就可以对 ...

  2. FFMPEG学习----使用SDL播放PCM数据

    参考雷神的代码: /** * 最简单的SDL2播放音频的例子(SDL2播放PCM) * Simplest Audio Play SDL2 (SDL2 play PCM) * * 本程序使用SDL2播放 ...

  3. FFmpeg学习3:播放音频

    参考dranger tutorial,本文将介绍如何使用FFmpeg解码音频数据,并使用SDL将解码后的数据输出. 本文主要包含以下几方面的内容: 关于播放音频的需要的一些基础知识介绍 使用SDL2播 ...

  4. FFMPEG学习----使用SDL播放YUV数据

    命令行下配置: G:\Coding\Video\SDL\proj>tree /F 文件夹 PATH 列表 卷序列号为 0FD5-0CC8 G:. │ sdl.cpp │ SDL2.dll │ S ...

  5. FFmpeg学习6:视音频同步

    在上一篇文章中,视频和音频是各自独立播放的,并不同步.本文主要描述了如何以音频的播放时长为基准,将视频同步到音频上以实现视音频的同步播放的.主要有以下几个方面的内容 视音频同步的简单介绍 DTS 和 ...

  6. 最简单的基于FFmpeg的封装格式处理:视音频分离器简化版(demuxer-simple)

    ===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...

  7. [转]FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  8. [总结]FFMPEG视音频编解码零基础学习方法--转

    ffmpeg编解码学习   目录(?)[-] ffmpeg程序的使用ffmpegexeffplayexeffprobeexe 1 ffmpegexe 2 ffplayexe 3 ffprobeexe ...

  9. FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

随机推荐

  1. 移动先行之谁主沉浮? 带着你的Net飞奔吧!

    移动系源码:https://github.com/dunitian/Windows10 移动系文档:https://github.com/dunitian/LoTDotNet/tree/master/ ...

  2. 【绝对干货】仿微信QQ设置图形头像裁剪,让你的App从此炫起来~

    最近在做毕业设计,想有一个功能和QQ一样可以裁剪头像并设置圆形头像,额,这是设计狮的一种潮流. 而纵观现在主流的APP,只要有用户系统这个功能,这个需求一般都是在(bu)劫(de)难(bu)逃(xue ...

  3. Asp.net Core准备工作

    1.安装环境 安装.Net Core SDK 安装VS2015 Update3 安装DotNetCore.1.0.1-VS2015Tools.Preview2.0.2.exe 2.新建Core工程 项 ...

  4. MyBatis源码分析(一)开篇

    源码学习的好处不用多说,Mybatis源码量少.逻辑简单,将写个系列文章来学习. SqlSession Mybatis的使用入口位于org.apache.ibatis.session包中的SqlSes ...

  5. BPM的魅力何在?

    BPM(Business Process Management , 企业流程管理平台) 是带动企业流程自动化的帮 手,也是最能忠实反应出企业作业流程问题症结的系统工具,在管理上,BPM可以让管理者利用 ...

  6. Android中Activity运行时屏幕方向与显示方式详解

    现在我们的手机一般都内置有方向感应器,手机屏幕会根据所处位置自动进行横竖屏切换(前提是未锁定屏幕方向).但有时我们的应用程序仅限在横屏或者竖屏状态下才可以运行,此时我们需要锁定该程序Activity运 ...

  7. Hadoop 2.x 生态系统及技术架构图

    一.负责收集数据的工具:Sqoop(关系型数据导入Hadoop)Flume(日志数据导入Hadoop,支持数据源广泛)Kafka(支持数据源有限,但吞吐大) 二.负责存储数据的工具:HBaseMong ...

  8. Create a bridge using a tagged vlan (8021.q) interface

    SOLUTION VERIFIED April 27 2013 KB26727 Environment Red Hat Enterprise Linux 5 Red Hat Enterprise Li ...

  9. [Django]用户权限学习系列之User权限基本操作指令

    针对Django 后台自带的用户管理系统,虽说感觉还可以,但是为了方便用户一些操作,特别设计自定义的用户权限管理系统. 在制作权限页面前,首先需要了解权限和用户配置权限的指令,上章讲到权限的添加,删除 ...

  10. 博客已经迁移至 http://barretlee.com/entry/,时而同步分享到这里

    博客园是一个十分好的写作平台,不过个人比较喜欢倒腾,所以将文章都做了搬迁. 博客已经迁移至 http://barretlee.com/entry/,感谢一直以来的关注和支持. 博客订阅地址: http ...