Files
oak-editor/app/decoder/ffmpeg/ffmpegdecoder.cpp
T
itsmattkc 663ae56ac4 more coherent render threading for now
Once again, conceptually this system should work, however it does not seem to
be the most efficient and it wouldn't surprise me if the multithreading was
eventually upgraded to an even more coherent system one day. However for
"core principles" this should be fairly decent.
2019-11-03 04:05:43 +11:00

779 lines
21 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#include "ffmpegdecoder.h"
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
#include <QDebug>
#include <QFile>
#include <QString>
#include <QtMath>
#include "common/filefunctions.h"
#include "common/timecodefunctions.h"
#include "decoder/wave.h"
#include "render/pixelservice.h"
FFmpegDecoder::FFmpegDecoder() :
fmt_ctx_(nullptr),
codec_ctx_(nullptr),
opts_(nullptr),
frame_(nullptr),
pkt_(nullptr),
scale_ctx_(nullptr)
{
}
FFmpegDecoder::~FFmpegDecoder()
{
Close();
}
bool FFmpegDecoder::Open()
{
if (open_) {
return true;
}
int error_code;
// Convert QString to a C strng
QByteArray ba = stream()->footage()->filename().toUtf8();
const char* filename = ba.constData();
// Open file in a format context
error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr);
// Handle format context error
if (error_code != 0) {
FFmpegError(error_code);
return false;
}
// Get stream information from format
error_code = avformat_find_stream_info(fmt_ctx_, nullptr);
// Handle get stream information error
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Dump format information
av_dump_format(fmt_ctx_, stream()->index(), filename, 0);
// Get reference to correct AVStream
avstream_ = fmt_ctx_->streams[stream()->index()];
// Find decoder
AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id);
// Handle failure to find decoder
if (codec == nullptr) {
Error(QStringLiteral("Failed to find appropriate decoder for this codec (%1 :: %2)")
.arg(stream()->footage()->filename(), avstream_->codecpar->codec_id));
return false;
}
// Allocate context for the decoder
codec_ctx_ = avcodec_alloc_context3(codec);
if (codec_ctx_ == nullptr) {
Error(QStringLiteral("Failed to allocate codec context (%1 :: %2)").arg(stream()->footage()->filename(), stream()->index()));
return false;
}
// Copy parameters from the AVStream to the AVCodecContext
error_code = avcodec_parameters_to_context(codec_ctx_, avstream_->codecpar);
// Handle failure to copy parameters
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// enable multithreading on decoding
error_code = av_dict_set(&opts_, "threads", "auto", 0);
// Handle failure to set multithreaded decoding
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Open codec
error_code = avcodec_open2(codec_ctx_, codec, &opts_);
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Set up
if (codec_ctx_->codec_type == AVMEDIA_TYPE_VIDEO) {
// Set up pixel format conversion for video
AVPixelFormat pix_fmt = static_cast<AVPixelFormat>(avstream_->codecpar->format);
// Get an Olive compatible AVPixelFormat
AVPixelFormat ideal_pix_fmt = GetCompatiblePixelFormat(pix_fmt);
// Determine which Olive native pixel format we retrieved
// Note that FFmpeg doesn't support float formats
switch (ideal_pix_fmt) {
case AV_PIX_FMT_RGBA:
output_fmt_ = olive::PIX_FMT_RGBA8;
break;
case AV_PIX_FMT_RGBA64:
output_fmt_ = olive::PIX_FMT_RGBA16U;
break;
default:
// We should never get here, but if we do there's nothing we can do with this format
return false;
}
scale_ctx_ = sws_getContext(avstream_->codecpar->width,
avstream_->codecpar->height,
pix_fmt,
avstream_->codecpar->width,
avstream_->codecpar->height,
ideal_pix_fmt,
0,
nullptr,
nullptr,
nullptr);
} else if (codec_ctx_->codec_type == AVMEDIA_TYPE_AUDIO) {
// FIXME: Fill this in
}
// Allocate a packet for reading
pkt_ = av_packet_alloc();
// Handle failure to create packet
if (pkt_ == nullptr) {
Error(QStringLiteral("Failed to allocate AVPacket"));
return false;
}
// Allocate a frame for decoding
frame_ = av_frame_alloc();
// Handle failure to create frame
if (frame_ == nullptr) {
Error(QStringLiteral("Failed to allocate AVFrame"));
return false;
}
// All allocation succeeded so we set the state to open
open_ = true;
return true;
}
FramePtr FFmpegDecoder::Retrieve(const rational &timecode, const rational &length)
{
if (!open_ && !Open()) {
return nullptr;
}
// Convert timecode to AVStream timebase
int64_t target_ts = GetTimestampFromTime(timecode);
if (target_ts < 0) {
Error(QStringLiteral("Index failed to produce a valid timestamp"));
return nullptr;
}
// Check if this is already the frame we have cached
if (frame_->pts != target_ts) {
// Cache FFmpeg error code returns
int ret = 0;
// Set up seeking loop
int64_t seek_ts = target_ts;
int64_t second_ts = qRound(rational(avstream_->time_base).flipped().toDouble());
bool got_frame = false;
bool last_backtrack = false;
// FFmpeg frame retrieve loop
while (ret >= 0 && frame_->pts != target_ts) {
// If the frame timestamp is too large, we need to seek back a little
if (got_frame && (frame_->pts > target_ts || frame_->pts == AV_NOPTS_VALUE)) {
// If we already tried seeking to 0 though, there's nothing we can do so we error here
if (last_backtrack) {
// Must be the earliest frame in the file
break;
}
// We can't seek earlier than 0, so if this is a 0-seek, don't try any more times after this attempt
if (seek_ts <= 0) {
seek_ts = 0;
last_backtrack = true;
}
Seek(seek_ts);
// FFmpeg doesn't always seek correctly, if we have to seek again we wrangle it into seeking back far enough
seek_ts -= second_ts;
}
ret = GetFrame();
got_frame = true;
}
// Handle any errors received during the frame retrieve process
if (ret < 0) {
FFmpegError(ret);
return nullptr;
}
}
// Frame was valid, now we convert it to a native Olive frame
FramePtr frame_container = Frame::Create();
frame_container->set_width(frame_->width);
frame_container->set_height(frame_->height);
frame_container->set_format(static_cast<olive::PixelFormat>(output_fmt_));
frame_container->set_timestamp(rational(frame_->pts * avstream_->time_base.num, avstream_->time_base.den));
frame_container->set_native_timestamp(frame_->pts);
frame_container->allocate();
// Convert pixel format/linesize if necessary
uint8_t* dst_data = frame_container->data();
int dst_linesize = frame_container->width() * PixelService::BytesPerPixel(static_cast<olive::PixelFormat>(output_fmt_));
// Perform pixel conversion
sws_scale(scale_ctx_,
frame_->data,
frame_->linesize,
0,
frame_->height,
&dst_data,
&dst_linesize);
// Audio decoding will use a length value eventually
Q_UNUSED(length)
return frame_container;
}
void FFmpegDecoder::Close()
{
frame_index_.clear();
if (scale_ctx_ != nullptr) {
sws_freeContext(scale_ctx_);
scale_ctx_ = nullptr;
}
if (pkt_ != nullptr) {
av_packet_free(&pkt_);
pkt_ = nullptr;
}
if (frame_ != nullptr) {
av_frame_free(&frame_);
frame_ = nullptr;
}
if (opts_ != nullptr) {
av_dict_free(&opts_);
opts_ = nullptr;
}
if (codec_ctx_ != nullptr) {
avcodec_free_context(&codec_ctx_);
codec_ctx_ = nullptr;
}
if (fmt_ctx_ != nullptr) {
avformat_close_input(&fmt_ctx_);
fmt_ctx_ = nullptr;
}
open_ = false;
}
QString FFmpegDecoder::id()
{
return "ffmpeg";
}
int64_t FFmpegDecoder::GetTimestampFromTime(const rational &time)
{
if (!open_ && !Open()) {
return -1;
}
// Convert timecode to AVStream timebase
int64_t target_ts = olive::time_to_timestamp(time, avstream_->time_base);
// Find closest actual timebase in the file
target_ts = GetClosestTimestampInIndex(target_ts);
return target_ts;
}
bool FFmpegDecoder::Probe(Footage *f)
{
if (open_) {
qWarning() << "Probe must be called while the Decoder is closed";
return false;
}
// Variable for receiving errors from FFmpeg
int error_code;
// Result to return
bool result = false;
// Convert QString to a C strng
QByteArray ba = f->filename().toUtf8();
const char* filename = ba.constData();
// Open file in a format context
error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr);
bool need_manual_duration = false;
// Handle format context error
if (error_code == 0) {
// Retrieve metadata about the media
av_dump_format(fmt_ctx_, 0, filename, 0);
// Dump it into the Footage object
for (unsigned int i=0;i<fmt_ctx_->nb_streams;i++) {
avstream_ = fmt_ctx_->streams[i];
StreamPtr str;
if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// Create a video stream object
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
video_stream->set_width(avstream_->codecpar->width);
video_stream->set_height(avstream_->codecpar->height);
video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx_, avstream_, nullptr));
str = video_stream;
} else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// Create an audio stream object
AudioStreamPtr audio_stream = std::make_shared<AudioStream>();
audio_stream->set_layout(avstream_->codecpar->channel_layout);
audio_stream->set_channels(avstream_->codecpar->channels);
audio_stream->set_sample_rate(avstream_->codecpar->sample_rate);
str = audio_stream;
} else {
// This is data we can't utilize at the moment, but we make a Stream object anyway to keep parity with the file
str = std::make_shared<Stream>();
// Set the correct codec type based on FFmpeg's result
switch (avstream_->codecpar->codec_type) {
case AVMEDIA_TYPE_UNKNOWN:
str->set_type(Stream::kUnknown);
break;
case AVMEDIA_TYPE_DATA:
str->set_type(Stream::kData);
break;
case AVMEDIA_TYPE_SUBTITLE:
str->set_type(Stream::kSubtitle);
break;
case AVMEDIA_TYPE_ATTACHMENT:
str->set_type(Stream::kAttachment);
break;
// We should never realistically get here, but we make an "invalid" stream just in case
default:
str->set_type(Stream::kUnknown);
break;
}
}
str->set_index(avstream_->index);
str->set_timebase(avstream_->time_base);
str->set_duration(avstream_->duration);
// The container/stream info may not contain a duration, so we'll need to manually retrieve it
if (avstream_->duration == AV_NOPTS_VALUE) {
need_manual_duration = true;
}
f->add_stream(str);
}
// As long as we can open the container and retrieve information, this was a successful probe
result = true;
}
// Free all memory
Close();
// If the metadata did not contain a duration, we'll need to loop through the file to retrieve it
if (need_manual_duration) {
// Index the first stream to retrieve the duration
set_stream(f->stream(0));
Open();
// Use index to find duration
// FIXME: Does nothing for sound
if (!LoadFrameIndex()) {
Index();
}
// Use last frame index as the duration
// FIXME: Does this skip the last frame?
int64_t duration = frame_index_.last();
f->stream(0)->set_duration(duration);
// Assume all durations are the same and set for each
for (int i=1;i<f->stream_count();i++) {
int64_t new_dur = av_rescale_q(duration,
f->stream(0)->timebase().toAVRational(),
f->stream(i)->timebase().toAVRational());
f->stream(i)->set_duration(new_dur);
}
Close();
}
return result;
}
void FFmpegDecoder::FFmpegError(int error_code)
{
char err[1024];
av_strerror(error_code, err, 1024);
Error(QStringLiteral("Error decoding %1 - %2 %3").arg(stream()->footage()->filename(),
QString::number(error_code),
err));
}
void FFmpegDecoder::Error(const QString &s)
{
qWarning() << s;
Close();
}
void FFmpegDecoder::Index()
{
if (!open_) {
qWarning() << "Indexing function tried to run while decoder was closed";
return;
}
// Reset state
Seek(0);
int ret = 0;
if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// This should be unnecessary, but just in case...
frame_index_.clear();
// Iterate through every single frame and get each timestamp
// NOTE: Expects no frames to have been read so far
while (true) {
ret = GetFrame();
if (ret < 0) {
break;
} else {
frame_index_.append(frame_->pts);
}
}
// Save index to file
SaveFrameIndex();
} else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// Iterate through each audio frame and extract the PCM data
WaveOutput wave_out("C:\\Users\\Matt\\AppData\\Local\\Temp\\temporary.wav", // FIXME: Hardcoded path
AudioRenderingParams(avstream_->codecpar->sample_rate,
avstream_->codecpar->channel_layout,
GetNativeSampleRate(static_cast<AVSampleFormat>(avstream_->codecpar->format))));
SwrContext* resampler = nullptr;
AVSampleFormat src_sample_fmt = static_cast<AVSampleFormat>(avstream_->codecpar->format);
AVSampleFormat dst_sample_fmt;
// We don't use planar types internally, so if this is a planar format convert it now
if (av_sample_fmt_is_planar(src_sample_fmt)) {
dst_sample_fmt = av_get_packed_sample_fmt(src_sample_fmt);
// Bizarrely, swr_alloc_set_opts() uses a signed int64 while most of FFmpeg uses unsigned. We cast here.
int64_t channel_layout = static_cast<int64_t>(avstream_->codecpar->channel_layout);
resampler = swr_alloc_set_opts(nullptr,
channel_layout,
dst_sample_fmt,
avstream_->codecpar->sample_rate,
channel_layout,
src_sample_fmt,
avstream_->codecpar->sample_rate,
0,
nullptr);
} else {
dst_sample_fmt = src_sample_fmt;
}
if (wave_out.open()) {
while (true) {
ret = GetFrame();
if (ret < 0) {
break;
} else {
// Calculate the byte size for this audio buffer
int buffer_size = av_samples_get_buffer_size(nullptr,
avstream_->codecpar->channels,
frame_->nb_samples,
dst_sample_fmt,
0); // FIXME: Documentation unclear - should this be 0 or 1?
uint8_t* resampler_output;
if (resampler != nullptr) {
// We must need to resample this (mainly just convert from planar to packed if necessary)
resampler_output = new uint8_t[buffer_size];
swr_convert(resampler,
&resampler_output,
frame_->nb_samples,
const_cast<const uint8_t **>(frame_->data),
frame_->nb_samples);
} else {
// No resampling required, we can write directly from te frame buffer
resampler_output = frame_->data[0];
}
// Write packed WAV data to the disk cache
wave_out.write(reinterpret_cast<char*>(resampler_output), buffer_size);
// If we allocated an output for the resampler, delete it here
if (resampler_output != frame_->data[0]) {
delete [] resampler_output;
}
}
}
wave_out.close();
} else {
qWarning() << "Failed to open WAVE output for indexing";
}
if (resampler != nullptr) {
swr_free(&resampler);
}
}
// Reset state
Seek(0);
}
QString FFmpegDecoder::GetIndexFilename()
{
if (!open_) {
qWarning() << "GetIndexFilename tried to run while decoder was closed";
return QString();
}
return GetMediaIndexFilename(GetUniqueFileIdentifier(stream()->footage()->filename()))
.append(QString::number(avstream_->index));
}
bool FFmpegDecoder::LoadFrameIndex()
{
// Load index from file
QFile index_file(GetIndexFilename());
if (!index_file.exists()) {
return false;
}
if (index_file.open(QFile::ReadOnly)) {
// Resize based on filesize
frame_index_.resize(static_cast<int>(static_cast<size_t>(index_file.size()) / sizeof(int64_t)));
// Read frame index into vector
index_file.read(reinterpret_cast<char*>(frame_index_.data()),
index_file.size());
index_file.close();
return true;
}
return false;
}
void FFmpegDecoder::SaveFrameIndex()
{
// Save index to file
QFile index_file(GetIndexFilename());
if (index_file.open(QFile::WriteOnly)) {
// Write index in binary
index_file.write(reinterpret_cast<const char*>(frame_index_.constData()),
frame_index_.size() * static_cast<int>(sizeof(int64_t)));
index_file.close();
} else {
qWarning() << QStringLiteral("Failed to save index for %1").arg(stream()->footage()->filename());
}
}
int FFmpegDecoder::GetFrame()
{
bool eof = false;
int ret;
// Clear any previous frames
av_frame_unref(frame_);
while ((ret = avcodec_receive_frame(codec_ctx_, frame_)) == AVERROR(EAGAIN) && !eof) {
// Find next packet in the correct stream index
do {
// Free buffer in packet if there is one
av_packet_unref(pkt_);
// Read packet from file
ret = av_read_frame(fmt_ctx_, pkt_);
} while (pkt_->stream_index != avstream_->index && ret >= 0);
if (ret == AVERROR_EOF) {
// Don't break so that receive gets called again, but don't try to read again
eof = true;
// Send a null packet to signal end of
avcodec_send_packet(codec_ctx_, nullptr);
} else if (ret < 0) {
// Handle other error
break;
} else {
// Successful read, send the packet
ret = avcodec_send_packet(codec_ctx_, pkt_);
// We don't need the packet anymore, so free it
av_packet_unref(pkt_);
if (ret < 0) {
break;
}
}
}
return ret;
}
AVPixelFormat FFmpegDecoder::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt)
{
AVPixelFormat possible_pix_fmts[] = {
AV_PIX_FMT_RGBA,
AV_PIX_FMT_RGBA64,
AV_PIX_FMT_NONE
};
return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts,
pix_fmt,
1,
nullptr);
}
olive::SampleFormat FFmpegDecoder::GetNativeSampleRate(const AVSampleFormat &smp_fmt)
{
switch (smp_fmt) {
case AV_SAMPLE_FMT_U8:
return olive::SAMPLE_FMT_U8;
case AV_SAMPLE_FMT_S16:
return olive::SAMPLE_FMT_S16;
case AV_SAMPLE_FMT_S32:
return olive::SAMPLE_FMT_S32;
case AV_SAMPLE_FMT_S64:
return olive::SAMPLE_FMT_S64;
case AV_SAMPLE_FMT_FLT:
return olive::SAMPLE_FMT_FLT;
case AV_SAMPLE_FMT_DBL:
return olive::SAMPLE_FMT_DBL;
case AV_SAMPLE_FMT_U8P :
case AV_SAMPLE_FMT_S16P:
case AV_SAMPLE_FMT_S32P:
case AV_SAMPLE_FMT_S64P:
case AV_SAMPLE_FMT_FLTP:
case AV_SAMPLE_FMT_DBLP:
case AV_SAMPLE_FMT_NONE:
case AV_SAMPLE_FMT_NB:
break;
}
return olive::SAMPLE_FMT_INVALID;
}
int64_t FFmpegDecoder::GetClosestTimestampInIndex(const int64_t &ts)
{
// Index now if we haven't already
if (frame_index_.isEmpty() && !LoadFrameIndex()) {
Index();
}
if (frame_index_.isEmpty()) {
return -1;
}
if (ts <= 0) {
return 0;
}
// Use index to find closest frame in file
for (int i=1;i<frame_index_.size();i++) {
if (frame_index_.at(i) == ts) {
return ts;
} else if (frame_index_.at(i) > ts) {
return frame_index_.at(i - 1);
}
}
return frame_index_.last();
}
void FFmpegDecoder::Seek(int64_t timestamp)
{
avcodec_flush_buffers(codec_ctx_);
av_seek_frame(fmt_ctx_, avstream_->index, timestamp, AVSEEK_FLAG_BACKWARD);
}