外贸网站商城,wordpress添加表,北京常规网络营销电话,xampp wordpress 太慢系列文章目录
基于 FFmpeg 的跨平台视频播放器简明教程#xff08;一#xff09;#xff1a;FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程#xff08;二#xff09;#xff1a;基础知识和解封装#xff08;demux#xff09;基于 FFmpeg 的跨平台视频…系列文章目录
基于 FFmpeg 的跨平台视频播放器简明教程一FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程二基础知识和解封装demux基于 FFmpeg 的跨平台视频播放器简明教程三视频解码基于 FFmpeg 的跨平台视频播放器简明教程四像素格式与格式转换基于 FFmpeg 的跨平台视频播放器简明教程五使用 SDL 播放视频基于 FFmpeg 的跨平台视频播放器简明教程六使用 SDL 播放音频和视频 文章目录 系列文章目录前言线程模型代码说明解封装线程视频解码线程音频解码线程定时器线程 小小的优化参考 前言
在上篇文章中 基于 FFmpeg 的跨平台视频播放器简明教程六使用 SDL 播放音频和视频我们能够同时播放画面和音频。其中 SDL 启动了一个音频线程每次需要音频数据时都会回调到我们定义的函数。现在我们需要对视频显示做同样的事情。这么做能让我们的代码更加模块化更容易使用。
本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 04: Spawning Threads。这个系列对新手较为友好但 2015 后就不再更新了以至于文章中的 ffmpeg api 已经被弃用了。幸运的是有人对该教程的代码进行重写使用了较新的 api你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文的代码在 ffmpeg_video_player_tutorial-my_tutorial04_02_threads。
线程模型
回看目前实现的代码它在主线程做了非常多的事情包括
处理事件循环读取 packet并进行解码显示 frame
因此我们需要做的是让这些工作分开具体的
解封装线程负责从文件中读取 packet并把这些 packet 分配到不同的 packet 队列中视频解码线程从 video packet 队列中读取 packet解码为 frame然后将解码后的 frame 放入 video frame 队列中音频解码线程从 audio packet 队列中读取 packet解码为 frame然后将解码后的 frame 放入 audio frame 队列中定时器线程隔一段时间例如 30 毫秒发送一个事件通知主线程显示视频SDL 音频线程由 SDL 创建通过回调方式获取音频数据进行播放主线程负责各模块的初始化及事件循环。
比较上一章节虽然线程 1 到 4 使事情看上去似乎更复杂了但你可以放心这些线程只是将原来复杂的任务拆分开整体上并没有比之前的代码更复杂。 代码说明
让我们看看每个线程都在做些什么进行代码层面上的解释
解封装线程
std::thread demux_thread([]() {AVPacket *packet{nullptr};for (; sdl_app.running;) {std::tie(ret, packet) decoder_ctx.demuxer.readPacket();ON_SCOPE_EXIT([packet] { av_packet_unref(packet); });// read end of file, just exit this threadif (ret AVERROR_EOF || packet nullptr) {break;}if (packet-stream_index decoder_ctx.video_stream_index) {decoder_ctx.video_packet_queue.cloneAndPush(packet);} else if (packet-stream_index decoder_ctx.audio_stream_indexdecoder_ctx.audio_packet_queue.cloneAndPush(packet);}}
});它不停地从 demuxer 中读取 packet并将 packet 放入不同的 packet queue 中
视频解码线程
std::thread video_decode_thread([]() {AVFrame *frame av_frame_alloc();if (frame nullptr) {printf(Could not allocate frame.\n);return -1;}ON_SCOPE_EXIT([frame] {av_frame_unref(frame);av_frame_free(frame);});for (; sdl_app.running;) {if (decoder_ctx.video_packet_queue.size() ! 0) {ret decodePacketAndPushToFrameQueue(decoder_ctx.video_packet_queue,decoder_ctx.video_codec, frame,decoder_ctx.video_frame_queue);RETURN_IF_ERROR_LOG(ret, decode video packet failed\n);}}return 0;
});它不停地从 video packet queue 中读取 packet 并进行解码并将解码后的数据放入 video frame queue 中
音频解码线程
std::thread audio_decode_thread([]() {AVFrame *frame av_frame_alloc();if (frame nullptr) {printf(Could not allocate frame.\n);return -1;}ON_SCOPE_EXIT([frame] {av_frame_unref(frame);av_frame_free(frame);});for (; sdl_app.running;) {if (decoder_ctx.audio_packet_queue.size() ! 0) {ret decodePacketAndPushToFrameQueue(decoder_ctx.audio_packet_queue,decoder_ctx.audio_codec, frame,decoder_ctx.audio_frame_queue);printf(%zd \n, decoder_ctx.audio_frame_queue.size());RETURN_IF_ERROR_LOG(ret, decode audio packet failed\n);}}return 0;
});它不停地从 audio packet queue 中读取 packet 并进行解码并将解码后的数据放入 audio frame queue 中
定时器线程
我们使用 SDL_AddTimer 来创建一个定时器参数解释
interval定时器的间隔时间单位为毫秒。callback定时器结束时调用的函数。这个函数的原型必须如下Uint32 callback(Uint32 interval, void *param);param传递给回调函数的参数。
static Uint32 sdlRefreshTimerCallback(Uint32 interval, void *param) {(void)(interval);SDL_Event event;event.type FF_REFRESH_EVENT;event.user.data1 param;SDL_PushEvent(event);return 0;}我们的定时器回调函数 sdlRefreshTimerCallback 它向 SDL 发送一个 FF_REFRESH_EVENT 事件主线程在接收到 FF_REFRESH_EVENT 事件后将会从 video frame queue 中 pop 一帧数据进行图像格式转换操作并使用 SDL Render 将其渲染到屏幕上。最后会再次启动一个定时器用来刷新下一帧。
小小的优化
现在各自线程处理各自的事情解封装线程是数据源头该线程在一个 for 循环中源源不断地读取 packet后续的解码线程也在源源不断地解码数据。我们播放一个 30fps 的视频大约每 33.33ms 播放一帧视频而解码的速度比 33.33 快多了也就是说现在的线程模型会会囤积非常多视频数据等待被播放。这是对内存的一种浪费我们不需要缓存这么多的视频帧。
解封装线程是所有数据的源头我们只要控制住源头的速度就能够控制整个 Pipeline 的速度。因此我们在解封装时对 packet queue 中的数据存量进行检查如果超过某个阈值那么就让解封装线程 sleep 一会控制下 pipeline 的速度。
std::thread demux_thread([]() {AVPacket *packet{nullptr};for (; sdl_app.running;) {// sleep if packet size in queue is very largeif (decoder_ctx.video_packet_sync_que.totalPacketSize() DecoderContext::MAX_VIDEOQ_SIZE ||decoder_ctx.audio_packet_sync_que.totalPacketSize() DecoderContext::MAX_AUDIOQ_SIZE) {std::this_thread::sleep_for(10ms);continue;}std::tie(ret, packet) decoder_ctx.demuxer.readPacket();ON_SCOPE_EXIT([packet] { av_packet_unref(packet); });// read end of file, just exit this threadif (ret AVERROR_EOF || packet nullptr) {sdl_app.running false;break;}if (packet-stream_index decoder_ctx.video_stream_index) {decoder_ctx.video_packet_sync_que.tryPush(packet);} else if (packet-stream_index decoder_ctx.audio_stream_index) {decoder_ctx.audio_packet_sync_que.tryPush(packet);}}});参考
An ffmpeg and SDL Tutorial - Tutorial 04: Spawning Threadsffmpeg_video_player_tutorial-my_tutorial04_02_threads