參考文章
說明
?????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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/50899.html
標籤:其他
