一、簡介
我們使用QT+ffmpeg實(shí)現(xiàn)一個(gè)播放器,這里我們主要是為了學(xué)習(xí)ffmpege了,而QT只是輔助的,所以播放器的界面搭建我們不在介紹,可以直接看代碼(界面搭建代碼)。
現(xiàn)在我們直接接入主題,ffmpeg的解封裝我們可以直接參考之前介紹的 FFmpeg音視頻解封裝格式
下面是使用FFmpeg實(shí)現(xiàn)音視頻播放器的流程圖

FFmpeg音視頻播放流程圖
二、讀出文件
在 VideoPlayer類里的play方法里實(shí)現(xiàn)文件的讀取
void VideoPlayer::play() {
if (_state == Playing) return;
// 狀態(tài)可能是:暫停、停止、正常完畢
// 開始線程:讀取文件
std::thread([this](){
readFile();
}).detach();// detach 等到readFile方法執(zhí)行完,這個(gè)線程就會(huì)銷毀
setState(Playing);
}
我們創(chuàng)建一個(gè)線程,在線程里做讀取文件的操作,線程thread調(diào)用detach方法表示等到readFile方法執(zhí)行完,這個(gè)線程就會(huì)銷毀
三、初始化
3.1 讀取文件
這里實(shí)現(xiàn)讀取文件的方法,里面主要是讀取文件
void VideoPlayer::readFile(){
// 返回結(jié)果
int ret = 0;
// 創(chuàng)建解封裝上下文、打開文件
ret = avformat_open_input(&_fmtCtx,_filename,nullptr,nullptr);
END(avformat_open_input);
// 檢索流信息
ret = avformat_find_stream_info(_fmtCtx,nullptr);
END(avformat_find_stream_info);
// 打印流信息到控制臺(tái)
av_dump_format(_fmtCtx,0,_filename,0);
fflush(stderr);
// 初始化音頻信息
if (initAudioInfo() < 0) {
goto end;
}
// 初始化視頻信息
if (initVideoInfo() < 0) {
goto end;
}
// 到此為止,初始化完畢
emit initFinished(this);
// 從輸入文件中讀取數(shù)據(jù)
// while (av_read_frame(_fmtCtx,pkt) == 0) {
// if (pkt->stream_index == _aStream->index) { // 讀取到的是音頻數(shù)據(jù)
// }else if(pkt->stream_index == _vStream->index){// 讀取到的是視頻數(shù)據(jù)
// }
// // 釋放pkt內(nèi)部指針指向的一些額外內(nèi)存
// av_packet_unref(pkt);
// if(ret < 0){
// goto end;
// }
// }
end:
avcodec_free_context(&_aDecodeCtx);
avcodec_free_context(&_vDecodeCtx);
avformat_close_input(&_fmtCtx);
}
3.2 初始化音頻信息和視頻信息
// 初始化音頻信息
int VideoPlayer::initAudioInfo() {
int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);
RET(initDecoder);
return 0;
}
// 初始化視頻信息
int VideoPlayer::initVideoInfo() {
int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);
RET(initDecoder);
return 0;
}
3.3 初始化解碼器
int VideoPlayer::initDecoder(AVCodecContext **decodeCtx,
AVStream **stream,
AVMediaType type) {
// 根據(jù)type尋找最合適的流信息
// 返回值是流索引
int ret = av_find_best_stream(_fmtCtx, type, -1, -1, nullptr, 0);
RET(av_find_best_stream);
// 檢驗(yàn)流
int streamIdx = ret;
*stream = _fmtCtx->streams[streamIdx];
if (!*stream) {
qDebug() << "stream is empty";
return -1;
}
// 為當(dāng)前流找到合適的解碼器
AVCodec *decoder = avcodec_find_decoder((*stream)->codecpar->codec_id);
if (!decoder) {
qDebug() << "decoder not found" << (*stream)->codecpar->codec_id;
return -1;
}
// 初始化解碼上下文
*decodeCtx = avcodec_alloc_context3(decoder);
if (!decodeCtx) {
qDebug() << "avcodec_alloc_context3 error";
return -1;
}
// 從流中拷貝參數(shù)到解碼上下文中
ret = avcodec_parameters_to_context(*decodeCtx, (*stream)->codecpar);
RET(avcodec_parameters_to_context);
// 打開解碼器
ret = avcodec_open2(*decodeCtx, decoder, nullptr);
RET(avcodec_open2);
return 0;
}
四、實(shí)現(xiàn)視頻時(shí)長
上面我們已經(jīng)進(jìn)行了解碼器的初始化,所以可以通過AVFormatContext來獲取時(shí)長。
在VideoPlayer類里提供getDuration方法,用于返回時(shí)長
int64_t VideoPlayer::getDuration(){
return _fmtCtx ? _fmtCtx->duration : 0;
}
我們?cè)谏厦孢M(jìn)行初始化后會(huì)調(diào)用emit initFinished(this);用于回調(diào)MainWindow類的onPlayerInitFinished方法,在這個(gè)方法里可以更新界面的時(shí)長顯示
void MainWindow::onPlayerInitFinished(VideoPlayer *player) {
int64_t duration = player->getDuration();
qDebug()<< duration;
// 設(shè)置一些slider的范圍
ui->currentSlider->setRange(0,duration);
// 設(shè)置label的文字
ui->durationLabel->setText(getTimeText(duration));
}
因?yàn)?code>getDuration方法返回的是微妙的時(shí)間戳,所以這里需要進(jìn)行轉(zhuǎn)換成時(shí)鐘,好進(jìn)行顯示。
QString MainWindow::getTimeText(int64_t value){
int64_t seconds = value / 1000000;
// int64_t timeUs = player->getDuration();
// int h = seconds / 3600;
// int m = (seconds % 3600) / 60;
// int m = (seconds / 60) % 60;
// int s = seconds % 60;
// int ms = timeUs / 1000 % 1000;//微妙
QString h = QString("0%1").arg(seconds / 3600).right(2);
QString m = QString("0%1").arg((seconds / 60) % 60).right(2);
QString s = QString("0%1").arg(seconds % 60).right(2);
QString ms = QString("%1").arg(value / 1000 % 1000);
qDebug()<< h<<m<<s<<ms;
return QString("%1:%2:%3").arg(h).arg(m).arg(s);
}