多线程播放视音频ffmpeg+SDL

2024-04-22 18:48

本文主要是介绍多线程播放视音频ffmpeg+SDL,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转自http://www.cnblogs.com/wangguchangqing/p/5887197.html


在前面的学习中,视频和音频的播放是分开进行的。这主要是为了学习的方便,经过一段时间的学习,对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 threadmedia.video->video_play(); // create video threadSDL_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);
}       
elseav_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中未发送数据的indexint audio_stream;          // audio流indexAVCodecContext *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); // playingreturn true;
}

4. 视频播放

4.1 VideoState

和音频播放类似,也封装了一个VideoState保存视频播放时所需的数据

struct VideoState
{PacketQueue* videoq;        // 保存的video packet的队列缓存int video_stream;          // index of video streamAVCodecContext *video_ctx; // have already be opened by avcodec_open2FrameQueue 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 codeabout timing, etc. we're just going toguess at a delay for now. You canincrease and decrease this value and hard codethe 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 screenSDL_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,这只是播放了十几分钟。


这篇关于多线程播放视音频ffmpeg+SDL的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/926576

相关文章

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

多线程解析报表

假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。 Way1 join import java.time.LocalTime;public class Main {public static void main(String[] args) thro

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

Java 多线程概述

多线程技术概述   1.线程与进程 进程:内存中运行的应用程序,每个进程都拥有一个独立的内存空间。线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换、并发执行,一个进程最少有一个线程,线程实际数是在进程基础之上的进一步划分,一个进程启动之后,进程之中的若干执行路径又可以划分成若干个线程 2.线程的调度 分时调度:所有线程轮流使用CPU的使用权,平均分配时间抢占式调度

Java 多线程的基本方式

Java 多线程的基本方式 基础实现两种方式: 通过实现Callable 接口方式(可得到返回值):

JAVA- 多线程

一,多线程的概念 1.并行与并发 并行:多个任务在同一时刻在cpu 上同时执行并发:多个任务在同一时刻在cpu 上交替执行 2.进程与线程 进程:就是操作系统中正在运行的一个应用程序。所以进程也就是“正在进行的程序”。(Windows系统中,我们可以在任务管理器中看 到进程) 线程:是程序运行的基本执行单元。当操作系统执行一个程序时, 会在系统中建立一个进程,该进程必须至少建立一个线

ffmpeg面向对象-待定

1.常用对象 rtsp拉流第一步都是avformat_open_input,其入参可以看下怎么用: AVFormatContext *fmt_ctx = NULL;result = avformat_open_input(&fmt_ctx, input_filename, NULL, NULL); 其中fmt_ctx 如何分配内存的?如下 int avformat_open_input(

一款支持同一个屏幕界面同时播放多个视频的视频播放软件

GridPlayer 是一款基于 VLC 的免费开源跨平台多视频同步播放工具,支持在一块屏幕上同时播放多个视频。其主要功能包括: 多视频播放:用户可以在一个窗口中同时播放任意数量的视频,数量仅受硬件性能限制。支持多种格式和流媒体:GridPlayer 支持所有由 VLC 支持的视频格式以及流媒体 URL(如 m3u8 链接)。自定义网格布局:用户可以配置播放器的网格布局,以适应不同的观看需求。硬

多线程篇(阻塞队列- LinkedBlockingDeque)(持续更新迭代)

目录 一、LinkedBlockingDeque是什么 二、核心属性详解 三、核心方法详解 addFirst(E e) offerFirst(E e) putFirst(E e) removeFirst() pollFirst() takeFirst() 其他 四、总结 一、LinkedBlockingDeque是什么 首先queue是一种数据结构,一个集合中