ffmpeg から期間情報 (およびその他) を取得する方法
少し前に ffmpeg をいじりましたが、学習曲線がかなり急であることがわかりました。OPが数か月前にこの質問をしたとしても、SOの他の人が同様のことをしようとしている場合に備えて、いくつかのコードを投稿します。以下の Open() 関数は完成していますが、多くのアサートがあり、適切なエラー処理が欠けています。
すぐにわかる違いの 1 つは、 avformat_open_input の代わりに av_open_input_file を使用したことです。また、 av_dump_formatも使用しませんでした。
特に H.264 と MPEG-2 では、デュレーションの計算が難しい場合があります。以下のdurationSecの計算方法を参照してください。
注: この例では、JUCE C++ Utility Libraryも使用しています。
注 2: このコードは、ffmpeg チュートリアルの修正版です。
void VideoCanvas::Open(const char* videoFileName)
{
Logger::writeToLog(String(L"Opening video file ") + videoFileName);
Close();
AVCodec *pCodec;
// register all formats and codecs
av_register_all();
// open video file
int ret = av_open_input_file(&pFormatCtx, videoFileName, NULL, 0, NULL);
if (ret != 0) {
Logger::writeToLog("Unable to open video file: " + String(videoFileName));
Close();
return;
}
// Retrieve stream information
ret = av_find_stream_info(pFormatCtx);
jassert(ret >= 0);
// Find the first video stream
videoStream = -1;
audioStream = -1;
for(int i=0; i<pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO && videoStream < 0) {
videoStream = i;
}
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audioStream < 0) {
audioStream = i;
}
} // end for i
jassert(videoStream != -1);
jassert(audioStream != -1);
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
jassert(pCodecCtx != nullptr);
/**
* This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented. For fixed-fps content,
* timebase should be 1/framerate and timestamp increments should be
* identically 1.
* - encoding: MUST be set by user.
* - decoding: Set by libavcodec.
*/
AVRational avr = pCodecCtx->time_base;
Logger::writeToLog("time_base = " + String(avr.num) + "/" + String(avr.den));
/**
* For some codecs, the time base is closer to the field rate than the frame rate.
* Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
* if no telecine is used ...
*
* Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
*/
ticksPerFrame = pCodecCtx->ticks_per_frame;
Logger::writeToLog("ticks_per_frame = " + String(pCodecCtx->ticks_per_frame));
durationSec = static_cast<double>(pFormatCtx->streams[videoStream]->duration) * static_cast<double>(ticksPerFrame) / static_cast<double>(avr.den);
double fH = durationSec / 3600.;
int H = static_cast<int>(fH);
double fM = (fH - H) * 60.;
int M = static_cast<int>(fM);
double fS = (fM - M) * 60.;
int S = static_cast<int>(fS);
Logger::writeToLog("Video stream duration = " + String(H) + "H " + String(M) + "M " + String(fS, 3) + "S");
// calculate frame rate based on time_base and ticks_per_frame
frameRate = static_cast<double>(avr.den) / static_cast<double>(avr.num * pCodecCtx->ticks_per_frame);
Logger::writeToLog("Frame rate = " + String(frameRate) );
// audio codec context
if (audioStream != -1) {
aCodecCtx = pFormatCtx->streams[audioStream]->codec;
Logger::writeToLog("Audio sample rate = " + String(aCodecCtx->sample_rate));
Logger::writeToLog("Audio channels = " + String(aCodecCtx->channels));
}
jassert(aCodecCtx != nullptr);
// format:
// The "S" in "S16SYS" stands for "signed", the 16 says that each sample is 16 bits long,
// and "SYS" means that the endian-order will depend on the system you are on. This is the
// format that avcodec_decode_audio2 will give us the audio in.
// open the audio codec
if (audioStream != -1) {
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if (!aCodec) {
Logger::writeToLog(L"Unsupported codec ID = " + String(aCodecCtx->codec_id) );
Close();
return; // TODO: should we just play video if audio codec doesn't work?
}
avcodec_open(aCodecCtx, aCodec);
}
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec == nullptr) {
jassert(false);
// fprintf(stderr, "Unsupported codec!\n");
//return -1; // Codec not found
}
// Open video codec
ret = avcodec_open(pCodecCtx, pCodec);
jassert(ret >= 0);
// Allocate video frame
pFrame=avcodec_alloc_frame();
jassert(pFrame != nullptr);
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
jassert(pFrameRGB != nullptr);
int numBytes = avpicture_get_size(PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height);
jassert(numBytes != 0);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
jassert(buffer != nullptr);
// note: the pixel format here is RGB, but sws_getContext() needs to be PIX_FMT_BGR24 to match (BGR)
// this might have to do w/ endian-ness....make sure this is platform independent
if (m_image != nullptr) delete m_image;
m_image = new Image(Image::ARGB, pCodecCtx->width, pCodecCtx->height, true);
int dstW = pCodecCtx->width; // don't rescale
int dstH = pCodecCtx->height;
Logger::writeToLog(L"Video width = " + String(dstW));
Logger::writeToLog(L"Video height = " + String(dstH));
// this should only have to be done once
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, dstW, dstH, PIX_FMT_RGB32, SWS_FAST_BILINEAR, NULL, NULL, NULL);
jassert(img_convert_ctx != nullptr);
setSize(pCodecCtx->width, pCodecCtx->height);
} // Open()