使用ffmpeg实现单线程异步的视频播放器
Posted CodeOfCC
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用ffmpeg实现单线程异步的视频播放器相关的知识,希望对你有一定的参考价值。
自定义播放器系列
第一章 视频渲染
第二章 音频(push)播放
第三章 音频(pull)播放
第四章 实现时钟同步
第五章 实现通用时钟同步
第六章 实现播放器(本章)
文章目录
前言
ffplay是一个不错的播放器,是基于多线程实现的,播放视频时一般至少有4个线程:读包线程、视频解码线程、音频解码线程、视频渲染线程。如果需要多路播放时,线程不可避免的有点多,比如需要播放8路视频时则需要32个线程,这样对性能的消耗还是比较大的。于是想到用单线程实现一个播放器,经过实践发现是可行的,播放本地文件时可以做到完全单线程、播放网络流时需要一个线程实现读包异步。
一、播放流程
二、关键实现
因为是基于单线程的播放器有些细节还是要注意的。
1.视频
(1)解码
解码时需要注意设置多线程解码或者硬解以确保解码速度,因为在单线程中解码过慢则会导致视频卡顿。
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
//打开解码器
if (avcodec_open2(decoder->codecContext, codec, &opts) < 0)
LOG_ERROR("Could not open codec");
av_dict_free(&opts);
return ERRORCODE_DECODER_OPENFAILED;
或者根据情况设置硬解码器
codec = avcodec_find_decoder_by_name("hevc_qsv");
//打开解码器
if (avcodec_open2(decoder->codecContext, codec, &opts) < 0)
LOG_ERROR("Could not open codec");
av_dict_free(&opts);
return ERRORCODE_DECODER_OPENFAILED;
2、音频
(1)修正时钟
虽然音频的播放是基于流的,时钟也可以按照播放的数据量计算,但是出现丢包或者定位的一些情况时,按照数据量累计的方式会导致时钟不正确,所以在解码后的数据放入播放队列时应该进行时钟修正。synchronize_setClockTime参考《c语言 将音视频时钟同步封装成通用模块》。在音频解码之后:
//读取解码的音频帧
av_fifo_generic_read(play->audio.decoder.fifoFrame, &frame, sizeof(AVFrame*), NULL);
//同步(修正)时钟
AVRational timebase = play->formatContext->streams[audio->decoder.streamIndex]->time_base;
//当前帧的时间戳
double pts = (double)frame->pts * timebase.num / timebase.den;
//减去播放队列剩余数据的时长就是当前的音频时钟
pts -= (double)av_audio_fifo_size(play->audio.playFifo) / play->audio.spec.freq;
synchronize_setClockTime(&play->synchronize, &play->synchronize.audio, pts);
//同步(修正)时钟--end
//写入播放队列
av_audio_fifo_write(play->audio.playFifo, (void**)&data, samples);
3、时钟同步
需要时钟同步的地方有3处,一处是音频解码后即上面的2、(1)。另外两处则是音频播放和视频渲染的地方。
(1)、音频播放
synchronize_updateAudio参考《c语言 将音视频时钟同步封装成通用模块》。
//sdl音频回调
static void audio_callback(void* userdata, uint8_t* stream, int len)
Play* play = (Play*)userdata;
//需要写入的数据量
samples = play->audio.spec.samples;
//时钟同步,获取应该写入的数据量,如果是同步到音频,则需要写入的数据量始终等于应该写入的数据量。
samples = synchronize_updateAudio(&play->synchronize, samples, play->audio.spec.freq);
//略
(2)、视频播放
在视频渲染处实现如下代码,其中synchronize_updateVideo参考《c语言 将音视频时钟同步封装成通用模块》。
//---------------时钟同步--------------
AVRational timebase = play->formatContext->streams[video->decoder.streamIndex]->time_base;
//计算视频帧的pts
double pts = frame->pts * (double)timebase.num / timebase.den;
//视频帧的持续时间
double duration = frame->pkt_duration * (double)timebase.num / timebase.den;
double delay = synchronize_updateVideo(&play->synchronize, pts, duration);
if (delay > 0)
//延时
play->wakeupTime = getCurrentTime() + delay;
return 0;
else if (delay < 0)
//丢帧
av_fifo_generic_read(video->decoder.fifoFrame, &frame, sizeof(AVFrame*), NULL);
av_frame_unref(frame);
av_frame_free(&frame);
return 0;
else
//播放
av_fifo_generic_read(video->decoder.fifoFrame, &frame, sizeof(AVFrame*), NULL);
//---------------时钟同步-------------- end
4、异步读包
如果是本地文件单线程播放是完全没有问题的。但是播放网络流时,由于av_read_frame不是异步的,网络状况差时会导致延时过高影响到其他部分功能的正常进行,所以只能是将读包的操作放到子线程执行,这里采用async、await的思想实现异步。
(1)、async
将av_read_frame的放到线程池中执行。
//异步读取包,子线程中调用此方法
static int packet_readAsync(void* arg)
Play* play = (Play*)arg;
play->eofPacket = av_read_frame(play->formatContext, &play->packet);
//回到播放线程处理包
play_beginInvoke(play, packet_readAwait, play);
return 0;
(2)、await
执行完成后通过消息队列通知播放器线程,将后续操作放在播放线程中执行
//异步读取包完成后的操作
static int packet_readAwait(void* arg)
Play* play = (Play*)arg;
if (play->eofPacket == 0)
if (play->packet.stream_index == play->video.decoder.streamIndex)
//写入视频包队
AVPacket* packet = av_packet_clone(&play->packet);
av_fifo_generic_write(play->video.decoder.fifoPacket, &packet, sizeof(AVPacket*), NULL);
else if (play->packet.stream_index == play->audio.decoder.streamIndex)
//写入音频包队
AVPacket* packet = av_packet_clone(&play->packet);
av_fifo_generic_write(play->audio.decoder.fifoPacket, &packet, sizeof(AVPacket*), NULL);
av_packet_unref(&play->packet);
else if (play->eofPacket == AVERROR_EOF)
play->eofPacket = 1;
//写入空包flush解码器中的缓存
AVPacket* packet = &play->packet;
if (play->audio.decoder.fifoPacket)
av_fifo_generic_write(play->audio.decoder.fifoPacket, &packet, sizeof(AVPacket*), NULL);
if (play->video.decoder.fifoPacket)
av_fifo_generic_write(play->video.decoder.fifoPacket, &packet, sizeof(AVPacket*), NULL);
else
LOG_ERROR("read packet erro!\\n");
play->exitFlag = 1;
play->isAsyncReading = 0;
return ERRORCODE_PACKET_READFRAMEFAILED;
play->isAsyncReading = 0;
return 0;
(3)、消息处理
在播放线程中调用如下方法,处理事件,当await方法抛入消息队列后,就可以通过消息循环获取await方法在播放线程中执行。
//事件处理
static void play_eventHandler(Play* play)
PlayMessage msg;
while (messageQueue_poll(&play->mq, &msg))
switch (msg.type)
case PLAYMESSAGETYPE_INVOKE:
SDL_ThreadFunction fn = (SDL_ThreadFunction)msg.param1;
fn(msg.param2);
break;
三、完整代码
完整代码c和c++都可以运行,使用ffmpeg4.3、sdl2。
main.c/cpp
#include <stdio.h>
#include <stdint.h>
#include "SDL.h"
#include<stdint.h>
#include<string.h>
#ifdef __cplusplus
extern "C"
#endif
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/avutil.h"
#include "libavutil/time.h"
#include "libavutil/audio_fifo.h"
#include "libswresample/swresample.h"
#ifdef __cplusplus
#endif
/************************************************************************
* @Project: play
* @Decription: 视频播放器
* 这是一个播放器,基于单线程实现的播放器。如果是播放本地文件可以做到完全单线程,播放网络流则读取包的时候是异步的,当然
* 主流程依然是单线程。目前是读取包始终异步,未作判断本地文件同步读包处理。
* @Verision: v0.0.0
* @Author: Xin Nie
* @Create: 2022/12/12 21:21:00
* @LastUpdate: 2022/12/12 21:21:00
************************************************************************
* Copyright @ 2022. All rights reserved.
************************************************************************/
/// <summary>
/// 消息队列
/// </summary>
typedef struct
//队列长度
int _capacity;
//消息对象大小
int _elementSize;
//队列
AVFifoBuffer* _queue;
//互斥变量
SDL_mutex* _mtx;
//条件变量
SDL_cond* _cv;
MessageQueue;
/// <summary>
/// 对象池
/// </summary>
typedef struct
//对象缓存
void* buffer;
//对象大小
int elementSize;
//对象个数
int arraySize;
//对象使用状态1使用,0未使用
int* _arrayUseState;
//互斥变量
SDL_mutex* _mtx;
//条件变量
SDL_cond* _cv;
OjectPool;
/// <summary>
/// 线程池
/// </summary>
typedef struct
//最大线程数
int maxThreadCount;
//线程信息对象池
OjectPool _pool;
ThreadPool;
/// <summary>
/// 线程信息
/// </summary>
typedef struct
//所属线程池
ThreadPool* _threadPool;
//线程句柄
SDL_Thread* _thread;
//消息队列
MessageQueue _queue;
//线程回调方法
SDL_ThreadFunction _fn;
//线程回调参数
void* _arg;
ThreadInfo;
//解码器
typedef struct
//解码上下文
AVCodecContext* codecContext;
//解码器
const AVCodec* codec;
//解码临时帧
AVFrame* frame;
//包队列
AVFifoBuffer* fifoPacket;
//帧队列
AVFifoBuffer* fifoFrame;
//流下标
int streamIndex;
//解码结束标记
int eofFrame;
Decoder;
/// <summary>
/// 时钟对象
/// </summary>
typedef struct
//起始时间
double startTime;
//当前pts
double currentPts;
Clock;
/// <summary>
/// 时钟同步类型
/// </summary>
typedef enum
//同步到音频
SYNCHRONIZETYPE_AUDIO,
//同步到视频
SYNCHRONIZETYPE_VIDEO,
//同步到绝对时钟
SYNCHRONIZETYPE_ABSOLUTE
SynchronizeType;
/// <summary>
/// 时钟同步对象
/// </summary>
typedef struct
/// <summary>
/// 音频时钟
/// </summary>
Clock audio;
/// <summary>
/// 视频时钟
/// </summary>
Clock video;
/// <summary>
/// 绝对时钟
/// </summary>
Clock absolute;
/// <summary>
/// 时钟同步类型
/// </summary>
SynchronizeType type;
/// <summary>
/// 估算的视频帧时长
/// </summary>
double estimateVideoDuration;
/// <summary>
/// 估算视频帧数
/// </summary>
double n;
Synchronize;
//视频模块
typedef struct
//解码器
Decoder decoder;
//输出格式
enum AVPixelFormat forcePixelFormat;
//重采样对象
struct SwsContext* swsContext;
//重采样缓存
uint8_t* swsBuffer;
//渲染器
SDL_Renderer* sdlRenderer;
//纹理
SDL_Texture* sdlTexture;
//窗口
SDL_Window* screen;
//窗口宽
int screen_w;
//窗口高
int screen_h;
//旋转角度
double angle;
//播放结束标记
int eofDisplay;
//播放开始标记
int sofDisplay;
Video;
//音频模块
typedef struct
//解码器
Decoder decoder;
//输出格式
enum AVSampleFormat forceSampleFormat;
//音频设备id
SDL_AudioDeviceID audioId;
//期望的音频设备参数
SDL_Audiospec wantedSpec;
//实际的音频设备参数
SDL_AudioSpec spec;
//重采样对象
struct SwrContext* swrContext;
//重采样缓存
uint8_t* swrBuffer;
//播放队列
AVAudioFifo* playFifo;
//播放队列互斥锁
SDL_mutex* mutex;
//累积的待播放采样数
int accumulateSamples;
//音量
int volume;
//声音混合buffer
uint8_t* mixBuffer;
//播放结束标记
int eofPlay;
//播放开始标记
int sofPlay;
Audio;
//播放器
typedef struct
//视频url
char* url;
//解复用上下文
AVFormatContext* formatContext;
//包
AVPacket packet;
//是否正在读取包
int isAsyncReading;
//包读取结束标记
int eofPacket;
//视频模块
Video video;
//音频模块
Audio audio;
//时钟同步
Synchronize synchronize;
//延时结束时间
double wakeupTime;
//播放一帧
int step;
//是否暂停
int isPaused;
//是否循环
int isLoop;
//退出标记
int exitFlag;
//消息队列
MessageQueue mq;
Play;
//播放消息类型
typedef enum
//调用方法
PLAYMESSAGETYPE_INVOKE
PlayMessageType;
//播放消息
typedef struct
PlayMessageType type;
void* param1;
void* param2;
PlayMessage;
//格式映射
static const struct TextureFormatEntry
enum AVPixelFormat format;
int texture_fmt;
sdl_texture_format_map[] =
AV_PIX_FMT_RGB8, SDL_PIXELFORMAT_RGB332 ,
AV_PIX_FMT_RGB444, SDL_PIXELFORMAT_RGB444 ,
AV_PIX_FMT_RGB555, SDL_PIXELFORMAT_RGB555 ,
AV_PIX_FMT_BGR555, SDL_PIXELFORMAT_BGR555 ,
AV_PIX_FMT_RGB565, SDL_PIXELFORMAT_RGB565 ,
AV_PIX_FMT_BGR565, SDL_PIXELFORMAT_BGR565 ,
AV_PIX_FMT_RGB24, SDL_PIXELFORMAT_RGB24 ,
AV_PIX_FMT_BGR24, SDL_PIXELFORMAT_BGR24 ,
AV_PIX_FMT_0RGB32, SDL_PIXELFORMAT_RGB888 ,
AV_PIX_FMT_0BGR32, SDL_PIXELFORMAT_BGR888 ,
AV_PIX_FMT_NE(RGB0, 0BGR), SDL_PIXELFORMAT_RGBX8888 ,
AV_PIX_FMT_NE(BGR0, 0RGB), SDL_PIXELFORMAT_BGRX8888 ,
AV_PIX_FMT_RGB32, SDL_PIXELFORMAT_ARGB8888 ,
AV_PIX_FMT_RGB32_1, SDL_PIXELFORMAT_RGBA8888 ,
AV_PIX_FMT_BGR32, SDL_PIXELFORMAT_ABGR8888 ,
AV_PIX_FMT_BGR32_1, SDL_PIXELFORMAT_BGRA8888 ,
AV_PIX_FMT_YUV420P, SDL_PIXELFORMAT_IYUV ,
AV_PIX_FMT_YUYV422, SDL_PIXELFORMAT_YUY2 ,
AV_PIX_FMT_UYVY422, SDL_PIXELFORMAT_UYVY ,
AV_PIX_FMT_NONE, SDL_PIXELFORMAT_UNKNOWN ,
;
/// <summary>
/// 错误码
/// </summary>
typedef enum
//无错误
ERRORCODE_NONE = 0,
//播放
ERRORCODE_PLAY_OPENINPUTSTREAMFAILED = -0xffff,//打开输入流失败
ERRORCODE_PLAY_VIDEOINITFAILED,//视频初始化失败
ERRORCODE_PLAY_AUDIOINITFAILED,//音频初始化失败
ERRORCODE_PLAY_LOOPERROR,//播放循环错误
ERRORCODE_PLAY_READPACKETERROR,//解包错误
ERRORCODE_PLAY_VIDEODECODEERROR,//视频解码错误
ERRORCODE_PLAY_AUDIODECODEERROR,//音频解码错误
ERRORCODE_PLAY_VIDEODISPLAYERROR,//视频播放错误
ERRORCODE_PLAY_AUDIOPLAYERROR,//音频播放错误
//解包
ERRORCODE_PACKET_CANNOTOPENINPUTSTREAM,//无法代码输入流
ERRORCODE_PACKET_CANNOTFINDSTREAMINFO,//查找不到流信息
ERRORCODE_PACKET_DIDNOTFINDDANYSTREAM,//找不到任何流
ERRORCODE_PACKET_READFRAMEFAILED,//读取包失败
//解码
ERRORCODE_DECODER_CANNOTALLOCATECONTEXT,//解码器上下文申请内存失败
ERRORCODE_DECODER_SETPARAMFAILED,//解码器上下文设置参数失败
ERRORCODE_DECODER_CANNOTFINDDECODER,//找不到解码器
ERRORCODE_DECODER_OPENFAILED,//打开解码器失败
ERRORCODE_DECODER_SENDPACKEDFAILED,//解码失败
ERRORCODE_DECODER_MISSINGASTREAMTODECODE,//缺少用于解码的流
//视频
ERRORCODE_VIDEO_DECODERINITFAILED,//音频解码器初始化失败
ERRORCODE_VIDEO_CANNOTGETSWSCONTEX,//无法获取ffmpeg swsContext
ERRORCODE_VIDEO_IMAGEFILLARRAYFAILED,//将图像数据映射到数组时失败:av_image_fill_arrays
ERRORCODE_VIDEO_CANNOTRESAMPLEAFRAME,//无法重采样视频帧
ERRORCODE_VIDEO_MISSINGSTREAM,//缺少视频流
//音频
ERRORCODE_AUDIO_DECODERINITFAILED,//音频解码器初始化失败
ERRORCODE_AUDIO_UNSUPORTDEVICESAMPLEFORMAT,//不支持音频设备采样格式
ERRORCODE_AUDIO_SAMPLESSIZEINVALID,//采样大小不合法
ERRORCODE_AUDIO_MISSINGSTREAM,//缺少音频流
ERRORCODE_AUDIO_SWRINITFAILED,//ffmpeg swr重采样对象初始化失败
ERRORCODE_AUDIO_CANNOTCONVERSAMPLE,//音频重采样失败
ERRORCODE_AUDIO_QUEUEISEMPTY,//队列数据为空
//帧
ERRORCODE_FRAME_ALLOCFAILED,//初始化帧失败
//队列
ERRORCODE_FIFO_ALLOCFAILED,//初始化队列失败
//sdl
ERRORCODE_SDL_INITFAILED,//sdl初始化失败
ERRORCODE_SDL_CANNOTCREATEMUTEX,//无法创建互斥锁
ERRORCODE_SDL_CANNOTOPENDEVICE, //无法打开音频设备
ERRORCODE_SDL_CREATEWINDOWFAILED,//创建窗口失败
ERRORCODE_SDL_CREATERENDERERFAILED,//创建渲染器失败
ERRORCODE_SDL_CREATETEXTUREFAILED,//创建纹理失败
//内存
ERRORCODE_MEMORY_ALLOCFAILED,//申请内存失败
ERRORCODE_MEMORY_LEAK,//内存泄漏
//参数
ERRORCODE_ARGUMENT_INVALIDQT+ffmpeg+多线程的视频播放器的基本使用
一、简述
视频播放器实质是快速的播放图片,一般情况下,人的眼睛一秒可以扫过25帧图片,这样看起来有视频播放的效果。有的人可能会想到通过把视频全部解码完,然后把解码后得到所有的图片保存下来再开始播放,那么问题来了,如果这个视频非常的大(有好几百兆),就会导致播放器都打开了迟迟没有响应的情况,使得体验感大大下降。
因此,我们可以进行边解码边播放,才有时效性,所以就引入了线程实现并发,界面是主线程,解码是子线程(详见我的博客:多线程的简单使用,这里不做过多阐述),下面就来实现边解码视频边在窗口播放的效果。
二、实现效果
部分视频转成动图
三、基本实现思路
四、核心代码
视频的解码和多线程封装成的类fdecode,fdecode继承QThread,重写虚函数run
1、解码线程类fdecode.cpp:
QImage desImage = QImage((uchar*)buffer,codec->width,codec->height,
QImage::Format_RGB32,nullptr,nullptr);//RGB像素数据
//每解码一帧图像给显示窗口发送一个显示图像的信号
emit sendImage(desImage);//触发信号
msleep(25);//播放倍速设置,可以通过延时来调
void fdecode::registerFFmpeg()
av_register_all();//注册所有组件
void fdecode::run()//里面执行的是整个解码的流程
this->registerFFmpeg();
this->fileName = "Warcraft3_End.avi";
this->openVideoStream(this->fileName);
2、播放界面类playwidget.cpp:
#include "playwidget.h"
playWidget::playWidget()
this->resize(800,368);
//先创建解码线程对象
this->fdec = new fdecode();
connect(fdec,SIGNAL(sendImage(QImage)),this,SLOT(receiveImage(QImage)));//注意sendImage和receiveImage的参数
//启动解码线程
fdec->start();
void playWidget::paintEvent(QPaintEvent *)
QPainter painter(this);
if(!this->img.isNull())
painter.drawImage(QRect(0,0,800,368),this->img);
void playWidget::receiveImage(QImage img)
this->img = img;
this->update();
3、main.cpp
详细思路:
1、得到的每一帧RGB图像怎么处理?
每解码一帧图像给播放界面发送一个显示图像的信号
2、解码得到的图片怎么在播放界面显示?
- 使用信号和槽的机制(.h文件中一定要加Q_OBJECT)信号不需要实现,槽要实现
- 在解码这边得到一张图片就发送一个信号,播放界面收到信号就显示图片
- 信号写在解码线程类fdecode里,信号的参数要大于等于槽函数的参数
3、信号在哪触发?
解码线程类fdecode中解码生成一帧图片的时候(用emit触发)
4、在哪接收信号?
- 播放界面类中定义槽(public slots)用来接收信号
5、在播放界面接收图片要关联信号和槽,在哪里关联呢?
- 在播放界面的构造函数里面用connect
6、在播放界面构造函数里进行创建解码线程对象、启动解码线程(启动时就会调用run函数)、关联信号和槽
connect四个参数:信号发送者(解码线程对象)、发什么信号、 谁接收信号 、做什么事情
7、界面上接收的图片一直在变,怎么办?
接收到一张图片要保存到播放界面类中(定义QImage类成员接收),然后再绘制图片(重绘事件void paintEvent(QPaintEvent *);)
8、什么时候触发QPaintEvent?
不会一直调用,receiveImage中接收一张图片后,调用更新函数update触发
注意:
(1) 发信号的是谁,signal就在谁的.h中声明,接收的是谁,槽函数就在谁的.h中声明
(2) connect 在创建了新线程后执行,之后执行线程的start函数
源码下载链接(包括演示视频):
QT+ffmpeg+多线程的视频播放器的代码-C/C++文档类资源-CSDN下载
以上是关于使用ffmpeg实现单线程异步的视频播放器的主要内容,如果未能解决你的问题,请参考以下文章