使用ffmpeg实现单线程异步的视频播放器

本文介绍如何使用ffmpeg实现一个单线程异步的视频播放器,适用于本地文件和网络流播放。通过解码优化、时钟同步和异步读包策略,减少线程消耗,提高性能。在播放本地文件时可以实现完全单线程,网络流播放时使用子线程读包。完整代码提供下载,适用于Windows和Linux平台。
摘要由CSDN通过智能技术生成

自定义播放器系列

第一章 视频渲染
第二章 音频(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>
/// 错误码
/// &
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值