自定义播放器系列
第一章 视频渲染
第二章 音频(push)播放
第三章 音频(pull)播放
第四章 实现时钟同步
第五章 实现通用时钟同步
第六章 实现播放器(本章)
前言
ffplay是一个不错的播放器,是基于多线程实现的,播放视频时一般至少有4个线程:读包线程、视频解码线程、音频解码线程、视频渲染线程。如果需要多路播放时,线程不可避免的有点多,比如需要播放8路视频时则需要32个线程,这样对性能的消耗还是比较大的。于是想到用单线程实现一个播放器,经过实践发现是可行的,播放本地文件时可以做到完全单线程、播放网络流时需要一个线程实现读包异步。
一、播放流程
![](https://i-blog.csdnimg.cn/blog_migrate/b8cb4a75b0a05d39b3fcbe2b07731947.png)
二、关键实现
因为是基于单线程的播放器有些细节还是要注意的。
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>
/// 错误码
/// &