/*
Copyright (C) 2011-2013 Arnaud Champenois arthelion92@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* FFMPEG C++ Wrapper
Thanks to http://dranger.com/ and QTFFmpegWrapper
2013 version : ported (a minima) to FFMPEG 2.1
*/
#include "ffmpegdecoder.h"
#include "utils.h"
#include <QFileInfo>
#include <QDebug>
#define TS_PACKET_SIZE 188
const int64_t theOffsetValue = 1LL<<33;
const AVRational FFMPEGDecoder::millisecondbase = {1, 1000};
FFMPEGDecoder::FFMPEGDecoder()
{
InitValues();
}
void FFMPEGDecoder::InitValues()
{
mFormatCtx = NULL;
mAudioCodecCtx = NULL;
mVideoCodecCtx = NULL;
mVideoStream=-1;
mAudioStream=-1;
mFrame = NULL;
mFrameRGB = NULL;
mImgConvertCtx = NULL;
mFirstFilePosition = -1;
mFilePosition = 0;
mTimePosition = 0;
mBytesPerSec = 0;
mOptW = mOptH = 0;
mFirstFrameTimeStamp = -1;
}
FFMPEGDecoder::~FFMPEGDecoder()
{
Close();
}
int64_t FFMPEGDecoder::MSToFilePos(int64_t aMsValue) const
{
int64_t aBitCount = (aMsValue*mBytesPerSec)/1000;
int64_t aPacketNb = aBitCount/TS_PACKET_SIZE;
return (aPacketNb*TS_PACKET_SIZE);
}
int64_t FFMPEGDecoder::FilePosToMS(int64_t aPosValue) const
{
return (aPosValue*1000)/mBytesPerSec;
}
bool FFMPEGDecoder::Open(const QString &aFileName,int aVideoStream, int anAudioStream)
{
if(avformat_open_input(&mFormatCtx, aFileName.toLocal8Bit(), NULL, NULL)!=0)
return false;
// Retrieve stream information
if(avformat_find_stream_info(mFormatCtx,NULL)<0)
return false; // Couldn't find stream information
QFile aFile(aFileName);
qint64 fileSize = aFile.size();
// Find the first video stream
mVideoStream=aVideoStream;
mAudioStream=anAudioStream;
for(unsigned i=0; i < mFormatCtx->nb_streams; i++) {
if(mFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO
&&
mVideoStream < 0) {
mVideoStream=i;
}
if(mFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
mAudioStream < 0) {
mAudioStream=i;
}
if(mVideoStream != -1 && mAudioStream != -1) break;
}
if(mVideoStream==-1 || mAudioStream==-1)
return false;
mVideoCodecCtx=mFormatCtx->streams[mVideoStream]->codec;
mAudioCodecCtx=mFormatCtx->streams[mAudioStream]->codec;
print_log(QString("TimeBase = %1,%2").arg(mFormatCtx->streams[mVideoStream]->time_base.num)
.arg(mFormatCtx->streams[mVideoStream]->time_base.den));
print_log(QString("Offset=%1").arg(theOffsetValue));
AVCodec *aCodec;
// Find the decoder for the video stream
aCodec=avcodec_find_decoder(mVideoCodecCtx->codec_id);
if(aCodec==NULL) {
return false; // Codec not found
}
if(avcodec_open2(mVideoCodecCtx, aCodec,0)<0)
return false; // Could not open codec
// Find the decoder for the audio stream
aCodec=avcodec_find_decoder(mAudioCodecCtx->codec_id);
if(aCodec==NULL) {
return false; // Codec not found
}
if(avcodec_open2(mAudioCodecCtx, aCodec,0)<0)
return false; // Could not open codec
// Allocate video frame
mFrame=av_frame_alloc();
mFrameRGB=av_frame_alloc();
// Hack to correct wrong frame rates that seem to be generated by some
// codecs
if(mVideoCodecCtx->time_base.num>1000 && mVideoCodecCtx->time_base.den==1)
mVideoCodecCtx->time_base.den=1000;
if(mFormatCtx->duration > 0 && mVideoCodecCtx->codec_id == CODEC_ID_H264) {
mBytesPerSec = (1000*fileSize)/TickToMS(mFormatCtx->streams[mVideoStream]->duration);
}
else
mBytesPerSec = ((105*mFormatCtx->bit_rate)/800);
print_log(QString("Rate B/s : %1").arg(mBytesPerSec));
// Determine required buffer size and allocate buffer
unsigned numBytes=avpicture_get_size(PIX_FMT_RGB24, mVideoCodecCtx->width,mVideoCodecCtx->height);
uint8_t *buffer=new uint8_t[numBytes];
// Assign appropriate parts of buffer to image planes in pFrameRGB
avpicture_fill((AVPicture *)mFrameRGB, buffer, PIX_FMT_RGB24,
mVideoCodecCtx->width, mVideoCodecCtx->height);
mFilePosition = 0;
mTimePosition = 0;
mFirstFrameTimeStamp = -1;
int64_t anANum,anADen;
GetAspectRatio(anANum,anADen);
int64_t aMax = (anANum>anADen) ?anANum:anADen;
int64_t aLocMax = aMax;
int aFactor = 2;
while(aLocMax<1000)
{
aLocMax = aMax*aFactor;
aFactor++;
}
mOptW = anANum*aFactor;
mOptH = anADen*aFactor;
readNextFrame();
SeekFrame(mTimePosition+1,FRAME_SEEK_SET);
readNextFrame();
mTimeGOP = mTimePosition;
qDebug() << "GOP=" << mTimeGOP;
Rewind();
return true;
}
void FFMPEGDecoder::Close()
{
if(mAudioCodecCtx)
avcodec_close(mAudioCodecCtx);
if(mVideoCodecCtx)
avcodec_close(mVideoCodecCtx);
if(mVideoCodecCtx)
avformat_close_input(&mFormatCtx);
if(mFrame)
av_free(mFrame);
if(mFrameRGB)
av_free(mFrameRGB);
InitValues();
}
bool
FFMPEGDecoder::ReadNextPacket(FFMPEGPacket&aPacket, bool aNeedKeyFrame)
{
aPacket.Invalid();
while(!aPacket.IsValid())
{
if(av_read_frame(mFormatCtx, &aPacket.GetPacket())<0)
return false;
else if(aPacket.GetStreamIndex()==mVideoStream &&
(!aNeedKeyFrame || aPacket.IsKeyFrame()))
aPacket.SetValidity(FFMPEGPacket::VIDEO_PACKET);
else if(!aNeedKeyFrame && aPacket.GetStreamIndex()==mAudioStream)
aPacket.SetValidity(FFMPEGPacket::AUDIO_PACKET);
else
av_free_packet(&aPacket.GetPacket());
}
print_log(QString("Packet : dts=%1 pts=%2 key=%3").arg(aPacket.GetPacket().dts).arg(aPacket.GetPacket().pts).arg(aPacket.GetPacket().flags));
return true;
}
int64_t
FFMPEGDecoder::GetMSPosition() const
{
return mTimePosition;
}
bool FFMPEGDecoder::readNextFrame(bool aNeedKeyFrame)
{
FFMPEGPacket aPacket;
int frameFinished = 0;
bool aSetFrame = false;
print_log(QString("\nRead Frame"));
bool prevKeyFrame = false;
while(!frameFinished)
{
while(ReadNextPacket(aPacket,(!aSetFrame)?aNeedKeyFrame:false) && aPacket.GetType() != FFMPEGPacket::VIDEO_PACKET);
if(!aSetFrame) {
aSetFrame = true;
mFilePosition = aPacket.GetPos();
}
if(!aPacket.IsValid())
return false;
avcodec_decode_video2(mVideoCodecCtx,mFrame,&frameFinished,&aPacket.GetPacket());
prevKeyFrame = (prevKeyFrame || aPacket.IsKeyFrame());
if(aNeedKeyFrame && frameFinished && !prevKeyFrame) {
frameFinished = false;
}
}
if(mFirstFrameTimeStamp == -1)
mFirstFrameTimeStamp = av_frame_get_best_effort_timestamp(mFrame);
int64_t stamp = av_frame_get_best_effort_timestamp(mFrame);
if(stamp<mFirstFrameTimeStamp)
stamp+=(1<<33);
mTimePosition = (stamp-mFirstFrameTimeStamp);
mTimePosition = av_rescale_q(mTimePosition,mFormatCtx->streams[mVideoStream]->time_base,AVRational{1, 1000});
if(mFirstFilePosition<0) {
mFirstFilePosition = mFilePosition;
}
print_log(QString("Frame pos=%1 ms=%3").arg(mFilePosition).arg(GetMSPosition()));
return true;
}
bool FFMPEGDecoder::decodeLastFrame(QImage &anImage)
{
int w = mVideoCodecCtx->width;
int h = mVideoCodecCtx->height;
if(mFrame->interlaced_frame)
avpicture_deinterlace ((AVPicture*)mFrame,
(AVPicture*)mFrame,
mVideoCodecCtx->pix_fmt,w,h);
mImgConvertCtx = sws_getCachedContext(mImgConvertCtx,w, h,
mVideoCodecCtx->pix_fmt,
w, h,
PIX_FMT_RGB24,
SWS_BICUBIC, NULL, NULL, NULL);
if(mImgConvertCtx == NULL)
return false;
sws_scale(mImgConvertCtx, mFrame->data, mFrame->linesize, 0, mVideoCodecCtx->height,
mFrameRGB->data, mFrameRGB->linesize);
// Convert the frame to QImage
anImage=QImage(w,h,QImage::Format_RGB888);
for(int y=0;y<h;y++)
memcpy(anImage.scanLine(y),
mFrameRGB->data[0]+y*mFrameRGB->linesize[0],w*3);
return true;
}
bool
FFMPEGDecoder::ReadNextImage(QImage &anImage, bool aNeedKeyFrame)
{
if(!readNextFrame(aNeedKeyFrame))
return false;
return decodeLastFrame(anImage);
}
bool FFMPEGDecoder::SeekFrame(VPosition aPos,SeekMode aMode)
{
return SeekFrame(aPos.mMsPos,aMode);
}
bool FFMPEGDecoder::SeekFrame(int64_t aMsPos,SeekMode aMode)
{
bool backWard = false;
if(aMode == FRAME_SEEK_CUR) {
if(aMsPos<=0) backWard = true;
aMsPos += mTimePosition;
}
else if(aMsPos<=mTimePosition) {
backWard = true;
}
qDebug() << "seek=" << std::MsToString(aMsPos);
if(aMsPos<0)
return false;
mTimePosition = aMsPos;
int64_t seek_target = av_rescale_q(aMsPos*AV_TIME_BASE/1000, AVRational{1, AV_TIME_BASE},
mFormatCtx->streams[mVideoStream]->time_base)+mFirstFrameTimeStamp;
if(seek_target>=(1<<33))
seek_target -= (1<<33);
if(av_seek_frame(mFormatCtx,mVideoStream,seek_target, backWard?AVSEEK_FLAG_BACKWARD:0)<0)
return false;
avcodec_flush_buffers(mVideoCodecCtx);
avcodec_flush_buffers(mAudioCodecCtx);
return true;
}
bool FFMPEGDecoder::SeekPosition(int64_t aPos)
{
if(av_seek_frame(mFormatCtx,mVideoStream,aPos, AVSEEK_FLAG_BYTE)<0)
return false;
avcodec_flush_buffers(mVideoCodecCtx);
avcodec_flush_buffers(mAudioCodecCtx);
return true;
}
void FFMPEGDecoder::Rewind()
{
if(av_seek_frame(mFormatCtx,mVideoStream,mFirstFilePosition, AVSEEK_FLAG_BYTE)<0)
return;
avcodec_flush_buffers(mVideoCodecCtx);
avcodec_flush_buffers(mAudioCodecCtx);
mFilePosition = mFirstFilePosition;
mTimePosition = 0;
}
bool FFMPEGDecoder::GetFileInfo(FFMPEGFileInfo &aFileInfo)
{
aFileInfo.mLength = av_rescale_q(mFormatCtx->streams[mVideoStream]->duration,
mFormatCtx->streams[mVideoStream]->time_base,millisecondbase);
aFileInfo.mFrameRate = av_q2d(mFormatCtx->streams[mVideoStream]->avg_frame_rate);
aFileInfo.mFrameNb = mFormatCtx->streams[mVideoStream]->nb_frames;
if(aFileInfo.mFrameNb == 0 && aFileInfo.mLength != 0 && aFileInfo.mFrameRate!=0)
aFileInfo.mFrameNb = (int64_t)floor(aFileInfo.mLength*aFileInfo.mFrameRate);
else if(aFileInfo.mLength==0 && aFileInfo.mFrameRate!=0 && aFileInfo.mFrameNb!=0)
aFileInfo.mLength = (int64_t)floor(aFileInfo.mFrameNb/aFileInfo.mFrameRate);
print_log(QString("Length=%1").arg(aFileInfo.mLength));
GetAspectRatio(aFileInfo.mAspectRatio[0],aFileInfo.mAspectRatio[1]);
aFileInfo.mWidth = mVideoCodecCtx->width;
aFileInfo.mHeight = mVideoCodecCtx->height;
GetOptSize(aFileInfo.mOptWidth,aFileInfo.mOptHeight);
if(aFileInfo.mLength == 0)
return false;
else return true;
}
void FFMPEGDecoder::GetAspectRatio(int64_t&aNum,int64_t&aDen)
{
aNum = mVideoCodecCtx->sample_aspect_ratio.num*mVideoCodecCtx->width;
aDen = mVideoCodecCtx->sample_aspect_ratio.den*mVideoCodecCtx->height;
int64_t aMax = (aNum>aDen) ?aNum:aDen;
for(int64_t i=2;i<aMax;i++)
{
if(aNum%i==0 && aDen%i==0)
{
aNum /= i;
aDen /= i;
i=1;
}
}
}