使用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_INVALID

QT+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实现单线程异步的视频播放器的主要内容,如果未能解决你的问题,请参考以下文章

FFmpeg学习5:多线程播放视音频

Android ijkplayer详解使用教程

在 ffmpeg 中从单个图像创建特定持续时间的视频

QT+ffmpeg+多线程的视频播放器的基本使用

音视频同步

通过ffmpeg反向播放视频