参考文章
说明
Linux的系统为:中标麒麟
Qt版本:5.9.6
FFmpeg版本:4.0
(备注:Qt与FFmpeg均为跨平台,所以本工程是可在其他系统运行)
效果
模块简介:
涉及视音频读码、队列缓冲、解码、同步、播放等。
FFmpeg+Qt在Linux系统下编写的视频播放器。
流程
PTS和DTS
音视频流中的每一帧都有时间相关的信息,其中PTS是播放时间,DTS是解码时间。音频的PTS和DTS是一致的,而某些视频各种中可能会存在DTS和PTS不一致的帧,我们这里主要通过PTS来控制播放时间。对解码后的AVFrame使用av_frame_get_best_effort_timestamp可以获取PTS。
time_base
我们注意到PTS是一个整形数据,time_base是PTS的单位,PTS乘以time_base即可得到实际时间。只有AVStream中获取的time_base才是对的,其他地方获取的可能会有问题。
音视频同步策略
一般来说有三种方式,音频同步到视频,视频同步到音频,音视频同步到外部时间。一般各个参数设置正确音频就能够以正常的速度播放,所以把视频同步到音频在一般情况下,是一个简单有效的同步策略。本文主要采取这个方式同步音视频,来展示相关的基本思路。
1、解协议、解封装、配置视音频
#define MSG(str) QMessageBox::warning(0,"error",str,QMessageBox::Ok)
/// ffmpeg共享
namespace FFMPEG_COMMON
{
/// 由错误码查找错误原因
static QString FindErrorString(const int &index)
{
char buf[256] = { 0};
av_strerror(index,buf,256);
return QString(buf);
}
}
/// 打开媒体前,记得先进行关闭媒体操作
void FFMPEG::CloseMedia()
{
/// 视音频索引
VideoIndex = AudioIndex = -1;
/// 解码器上下文
avcodec_close(VCodecContext);
avcodec_free_context(&VCodecContext);
avcodec_close(ACodecContext);
avcodec_free_context(&ACodecContext);
/// 格式上下文
avformat_close_input(&avFormatContext);
}
/// 打开媒体 - 解协议、解封装、配置视音频
bool FFMPEG::OpenMedia(const char *url)
{
/// 打开媒体状态
bool OpenMediaState = false;
/// 检测媒体路径是否为空
short urlLen = strlen(url);
if(urlLen > 0)
{
/// 先关闭媒体文件,以保证打开顺利
CloseMedia();
/// 执行结果
int result = -1;
/// 打开输入上下文
result = avformat_open_input(&avFormatContext,url,nullptr,nullptr);
if(result != 0)
{
MSG("avformat_open_input - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
/// 打开媒体信息
result = avformat_find_stream_info(avFormatContext,nullptr);
if(result < 0)
{
MSG("avformat_find_stream_info - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
视频媒体处理
/// 查找媒体码流索引
VideoIndex = av_find_best_stream(avFormatContext,AVMediaType::AVMEDIA_TYPE_VIDEO,-1,-1,nullptr,0);
/// 查找解码器
AVCodec *VCodec = avcodec_find_decoder(avFormatContext->streams[VideoIndex]->codecpar->codec_id);
if(!VCodec)
{
MSG("video - \"avcodec_find_decoder\" error!");
return OpenMediaState;
}
/// 创建媒体解码上下文
VCodecContext = avcodec_alloc_context3(VCodec);
if(!VCodecContext)
{
MSG("video - \"avcodec_alloc_context3\" error!");
return OpenMediaState;
}
/// 配置媒体解码器上下文
result = avcodec_parameters_to_context(VCodecContext,avFormatContext->streams[VideoIndex]->codecpar);
if(result < 0)
{
MSG("video - avcodec_parameters_to_context - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
/// 打开媒体解码器上下文
result = avcodec_open2(VCodecContext,VCodec,nullptr);
if(result != 0)
{
MSG("video - avcodec_open2 - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
/// 解码器指针置空
VCodec = nullptr;
音频媒体处理
/// 查找媒体码流索引
AudioIndex = av_find_best_stream(avFormatContext,AVMediaType::AVMEDIA_TYPE_AUDIO,-1,-1,nullptr,0);
/// 查找解码器
AVCodec *ACodec = avcodec_find_decoder(avFormatContext->streams[AudioIndex]->codecpar->codec_id);
if(!ACodec)
{
MSG("audio - \"avcodec_find_decoder\" error!");
return OpenMediaState;
}
/// 创建媒体解码上下文
ACodecContext = avcodec_alloc_context3(ACodec);
if(!ACodecContext)
{
MSG("audio - \"avcodec_alloc_context3\" error!");
return OpenMediaState;
}
ACodecContext->thread_count = 4;
/// 配置媒体解码器上下文
result = avcodec_parameters_to_context(ACodecContext,avFormatContext->streams[AudioIndex]->codecpar);
if(result < 0)
{
MSG("audio - avcodec_parameters_to_context - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
/// 打开媒体解码器上下文
result = avcodec_open2(ACodecContext,ACodec,nullptr);
if(result != 0)
{
MSG("audio - avcodec_open2 - " + FFMPEG_COMMON::FindErrorString(result));
return OpenMediaState;
}
/// 解码器指针置空
ACodec = nullptr;
/// 打开媒体状态完毕
OpenMediaState = true;
}
/// 文件指针设置空
url = nullptr;
/// 返回打开媒体状态
return OpenMediaState;
}
2、循环读取,分配队列
/// 下列代码为线程内运行,将读取到的数据判断是否为音频或视频后,分别扔入对应的音视频线程
while(ThreadFlag)
{
/// 读帧
if(av_read_frame(avFormatContext,avPacket) == 0)
{
/// 判断帧类型
if(avPacket->stream_index == VideoIndex) /// 视频
{
/ 拷贝新的AVPacket
AVPacket *VPacket = av_packet_clone(avPacket);
if(VPacket)
{
/// 视频包加入缓冲队列。若缓冲队列满了则释放传入的视频包
video->Push(VPacket);
}
}
else if(avPacket->stream_index == AudioIndex) /// 音频
{
/// 拷贝新的AVPacket
AVPacket *APacket = av_packet_clone(avPacket);
if(APacket)
{
/// 视频包加入缓冲队列。若缓冲队列满了则释放传入的音频包
audio->Push(APacket);
}
}
/// 引用计数
av_packet_unref(avPacket);
}
else
{
/// 判断音频、视频的另外两个线程是否结束。若结束则退出本线程循环
if(video->isFinished() && audio->isFinished())
break;
}
QThread::msleep(10);
}
3、视音频解码单例
/// 进行解码
bool DeCodec::Send(AVCodecContext *avCodecContext,AVPacket *avPacket)
{
return avcodec_send_packet(avCodecContext,avPacket) == 0 ? true:false;
}
/// 解码后读帧 - 一次解码可能对应多次解码后读帧(一般音频会有)
bool DeCodec::Receive(AVCodecContext *avCodecContext,AVFrame *avFrame)
{
return avcodec_receive_frame(avCodecContext,avFrame) == 0 ? true:false;
}
4、音频播放
H
#ifndef AUDIOPLAY_H
#define AUDIOPLAY_H
#include <QAudioOutput>
#include <QIODevice>
class AudioPlay
{
public:
AudioPlay(){ }
~AudioPlay();
/// 设置参数
void SetParameter(const int &,const int &,const int &);
/// 返回缓冲内还没有播放的时间(ms)
long long GetPts();
/// 打开音频播放
virtual bool Open();
virtual void Close();
//播放音频
bool Write(const unsigned char *data, int datasize);
int GetFree();
private:
QAudioOutput *AudioOutput = nullptr;
QIODevice *AIODevice = nullptr;
int sampleRate = 44100;
int sampleSize = 16;
int channels = 2;
};
#endif // AUDIOPLAY_H
CPP
#include "AudioPlay.h"
AudioPlay::~AudioPlay()
{
Close();
}
void AudioPlay::SetParameter(const int &sr, const int &ss, const int &c)
{
sampleRate = sr;
sampleSize = ss;
channels = c;
}
long long AudioPlay::GetPts()
{
long long pts = 0;
/// 还没播放的音频数
double size = AudioOutput->bufferSize() - AudioOutput->bytesFree();
/// 1秒音频字节数大小
double secSize = sampleRate * (sampleSize / 8) *channels;
if(secSize > 0)
{
pts = (secSize / size) * 1000;
}
return pts;
}
bool AudioPlay::Open()
{
Close();
QAudioFormat AudioFormat;
AudioFormat.setSampleRate(sampleRate);
AudioFormat.setSampleSize(sampleSize);
AudioFormat.setChannelCount(channels);
AudioFormat.setCodec("audio/pcm");
AudioFormat.setByteOrder(QAudioFormat::LittleEndian);
AudioFormat.setSampleType(QAudioFormat::UnSignedInt);
AudioOutput = new QAudioOutput(AudioFormat);
AIODevice = AudioOutput->start(); //开始播放
if(AIODevice)
return true;
return false;
}
void AudioPlay::Close()
{
if (AIODevice)
{
if(AIODevice->isOpen())
AIODevice->close ();
}
AIODevice = nullptr;
if (AudioOutput)
{
AudioOutput->stop();
delete AudioOutput;
}
AudioOutput = nullptr;
}
bool AudioPlay::Write(const unsigned char *data, int datasize)
{
if (!data || datasize <= 0)
return false;
if (!AudioOutput || !AIODevice)
{
return false;
}
int size = AIODevice->write((char *)data, datasize);
if (datasize != size)
return false;
return true;
}
int AudioPlay::GetFree()
{
if (!AudioOutput)
{
return 0;
}
int free = AudioOutput->bytesFree();
return free;
}
5、音频线程处理 ,并使用QAudioOutput进行配置播放
/// 音频重采样上下文初始化
SwrContext *swrContext = swr_alloc();
swrContext = swr_alloc_set_opts(
swrContext,
av_get_default_channel_layout(2), // 输出格式
AV_SAMPLE_FMT_S16, // 输出样本格式
ACodecContext->sample_rate, // 输出采样率
av_get_default_channel_layout(ACodecContext->channels), // 输入格式
ACodecContext->sample_fmt, // 输入样本格式
ACodecContext->sample_rate, // 输入采样率
0,0
);
int result = swr_init(swrContext);
if(result !=0 )
{
qDebug() << "音频初始化失败!" << endl;
ThreadFlag = false;
}
unsigned char *pcm = nullptr;
/// 当空包累加次数为10则表明包读完
unsigned short PacketEmptyCnt = 0;
while(ThreadFlag)
{
if(!APackets.isEmpty())
{
PacketEmptyCnt = 0;
/// 取出一包进行处理
Mutex.lock();
AVPacket *APacket = APackets.front();
APackets.pop_front();
Mutex.unlock();
if(APacket)
{
/// 进行解码
bool SendFlag = DECODEC->Send(ACodecContext,APacket);
/// 解码成功
if(SendFlag)
{
/// 从解码处取出,一次解码对应多次接收成功
while(ThreadFlag)
{
/// 创建临时接收解码帧
AVFrame *AFrame = av_frame_alloc();
/// 接收解码帧
bool ReceiveFlag = DECODEC->Receive(ACodecContext,AFrame);
/// 接收解码帧成功
if(ReceiveFlag)
{
/// 获取音频pts
DECODEC->AudioPts = AFrame->pkt_pts * av_q2d(TimeBase);
uint8_t *data[2] = { 0};
if(!pcm)
{
pcm = new uint8_t[AFrame->nb_samples*2*2];
}
data[0] = pcm;
int result = swr_convert
(
swrContext,
data,AFrame->nb_samples, /// 输出
(const uint8_t **)(AFrame->data),AFrame->nb_samples /// 输入
);
int size = result * AFrame->channels * av_get_bytes_per_sample((AVSampleFormat)AV_SAMPLE_FMT_S16);
while(ThreadFlag)
{
if (size <= 0)
break;
/// 缓冲未播完,空间不够
if (audioPlay->GetFree() < size)
{
//msleep(1);
continue;
}
audioPlay->Write(pcm, size);
break;
}
}
/// 释放这一帧
av_frame_free(&AFrame);
/// 若接收解码帧失败则退出循环接收
if(!ReceiveFlag)
{
break;
}
}
}
/// 释放这一包
av_packet_free(&APacket);
}
}
else
{
if(++PacketEmptyCnt >= 10000)
{
break;
}
}
QThread::usleep(1);
}
6、视频线程处理 ,并转为QImage播放
/// 取出一包进行处理
Mutex.lock();
AVPacket *VPacket = VPackets.front();
VPackets.pop_front();
Mutex.unlock();
if(VPacket)
{
/// 进行解码
bool SendFlag = DECODEC->Send(VCodecContext,VPacket);
/// 解码成功
if(SendFlag)
{
/// 从解码处取出,一次解码对应多次接收成功
while(ThreadFlag)
{
/// 创建临时接收解码帧
AVFrame *VFrame = av_frame_alloc();
/// 接收解码帧
bool ReceiveFlag = DECODEC->Receive(VCodecContext,VFrame);
/// 接收解码帧成功
if(ReceiveFlag)
{
/// 计算视频帧的pts - 为下面同步做基础
if(VPacket->pts == AV_NOPTS_VALUE)
DECODEC->VideoPts = 0.0;
else
DECODEC->VideoPts = av_frame_get_best_effort_timestamp(VFrame) * av_q2d(TimeBase);
///
swsContext = sws_getCachedContext
(
swsContext, /// 传nullptr会新创建
VFrame->width, /// 输入宽
VFrame->height, /// 输入高
(AVPixelFormat)(VFrame->format), /// 输入格式
VFrame->width, /// 输出宽
VFrame->height, /// 输出高
AV_PIX_FMT_BGRA, /// 输出格式
SWS_FAST_BILINEAR, /// 转换算法
nullptr,
nullptr,
nullptr
);
if(swsContext)
{
if(rgb == nullptr)
{
rgb = new unsigned char[VFrame->width*VFrame->height*4];
}
uint8_t *data[2]={ 0};
data[0] = rgb;
int lines[2] = { 0};
lines[0] = VFrame->width * 4;
sws_scale
(
swsContext,
VFrame->data,
VFrame->linesize,
0,
VCodecContext->height,
avFrame_Dst->data,
avFrame_Dst->linesize
);
QImage Image((uchar *)dBuffer,VCodecContext->width,VCodecContext->height,QImage::Format_RGB32);
if(!Image.isNull())
{
VideoImage(Image);
}
}
}
最后 - 视音频同步
/// - 音视频同步
double frameRate = av_q2d(AgvTimeBase);
frameRate += VFrame->repeat_pict * (frameRate * 0.5);
if (fabs(DECODEC->VideoPts - DECODEC->AudioPts) > 0.04 &&
fabs(DECODEC->VideoPts - DECODEC->AudioPts) < 10.0)
{
/// 如果视频比音频快,延迟差值播放,否则直接播放,这里没有做丢帧处理
if (DECODEC->VideoPts > DECODEC->AudioPts)
{
/// 除4是为了画面流畅
QThread::usleep((unsigned long)((DECODEC->VideoPts - DECODEC->AudioPts)*1000000/4));
}
}
///
源码
正在上传…
关注
微信公众号搜索"Qt_io_"或"Qt开发者中心"了解更多关于Qt、C++开发知识.。
笔者 - jxd