merged encoder and decoder code into one folder since they can share quite a bit of code

This commit is contained in:
itsmattkc
2019-12-21 20:03:25 +11:00
parent 15e01c9452
commit ee31da9e71
25 changed files with 744 additions and 210 deletions
+26
View File
@@ -0,0 +1,26 @@
# 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/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
codec/ffmpeg/ffmpegcommon.h
codec/ffmpeg/ffmpegcommon.cpp
codec/ffmpeg/ffmpegdecoder.h
codec/ffmpeg/ffmpegdecoder.cpp
codec/ffmpeg/ffmpegencoder.h
codec/ffmpeg/ffmpegencoder.cpp
PARENT_SCOPE
)
+101
View File
@@ -0,0 +1,101 @@
#include "ffmpegcommon.h"
AVPixelFormat FFmpegCommon::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);
}
SampleFormat FFmpegCommon::GetNativeSampleFormat(const AVSampleFormat &smp_fmt)
{
switch (smp_fmt) {
case AV_SAMPLE_FMT_U8:
return SAMPLE_FMT_U8;
case AV_SAMPLE_FMT_S16:
return SAMPLE_FMT_S16;
case AV_SAMPLE_FMT_S32:
return SAMPLE_FMT_S32;
case AV_SAMPLE_FMT_S64:
return SAMPLE_FMT_S64;
case AV_SAMPLE_FMT_FLT:
return SAMPLE_FMT_FLT;
case AV_SAMPLE_FMT_DBL:
return 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 SAMPLE_FMT_INVALID;
}
AVSampleFormat FFmpegCommon::GetFFmpegSampleFormat(const SampleFormat &smp_fmt)
{
switch (smp_fmt) {
case SAMPLE_FMT_U8:
return AV_SAMPLE_FMT_U8;
case SAMPLE_FMT_S16:
return AV_SAMPLE_FMT_S16;
case SAMPLE_FMT_S32:
return AV_SAMPLE_FMT_S32;
case SAMPLE_FMT_S64:
return AV_SAMPLE_FMT_S64;
case SAMPLE_FMT_FLT:
return AV_SAMPLE_FMT_FLT;
case SAMPLE_FMT_DBL:
return AV_SAMPLE_FMT_DBL;
case SAMPLE_FMT_INVALID:
case SAMPLE_FMT_COUNT:
break;
}
return AV_SAMPLE_FMT_NONE;
}
AVPixelFormat FFmpegCommon::GetFFmpegPixelFormat(const olive::PixelFormat &pix_fmt)
{
switch (pix_fmt) {
case olive::PIX_FMT_RGBA8:
return AV_PIX_FMT_RGBA;
case olive::PIX_FMT_RGBA16U:
return AV_PIX_FMT_RGBA64;
case olive::PIX_FMT_RGBA16F:
case olive::PIX_FMT_RGBA32F:
case olive::PIX_FMT_INVALID:
case olive::PIX_FMT_COUNT:
break;
}
return AV_PIX_FMT_NONE;
}
olive::PixelFormat FFmpegCommon::GetCompatiblePixelFormat(const olive::PixelFormat &pix_fmt)
{
switch (pix_fmt) {
case olive::PIX_FMT_RGBA8:
return olive::PIX_FMT_RGBA8;
case olive::PIX_FMT_RGBA16U:
case olive::PIX_FMT_RGBA16F:
case olive::PIX_FMT_RGBA32F:
return olive::PIX_FMT_RGBA16U;
case olive::PIX_FMT_INVALID:
case olive::PIX_FMT_COUNT:
break;
}
return olive::PIX_FMT_INVALID;
}
+39
View File
@@ -0,0 +1,39 @@
#ifndef FFMPEGABSTRACTION_H
#define FFMPEGABSTRACTION_H
extern "C" {
#include <libavformat/avformat.h>
}
#include "audio/sampleformat.h"
#include "render/pixelformat.h"
class FFmpegCommon {
public:
/**
* @brief Returns an AVPixelFormat that can be used to convert a frame to a data type Olive supports with minimal data loss
*/
static AVPixelFormat GetCompatiblePixelFormat(const AVPixelFormat& pix_fmt);
/**
* @brief Returns a native pixel format that can be used to convert from a native frame to an AVFrame with minimal data loss
*/
static olive::PixelFormat GetCompatiblePixelFormat(const olive::PixelFormat& pix_fmt);
/**
* @brief Returns an FFmpeg pixel format for a given native pixel format
*/
static AVPixelFormat GetFFmpegPixelFormat(const olive::PixelFormat& pix_fmt);
/**
* @brief Returns a native sample format type for a given AVSampleFormat
*/
static SampleFormat GetNativeSampleFormat(const AVSampleFormat& smp_fmt);
/**
* @brief Returns an FFmpeg sample format type for a given native type
*/
static AVSampleFormat GetFFmpegSampleFormat(const SampleFormat& smp_fmt);
};
#endif // FFMPEGABSTRACTION_H
+961
View File
@@ -0,0 +1,961 @@
/***
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 <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/pixdesc.h>
}
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QString>
#include <QtMath>
#include "codec/waveinput.h"
#include "common/filefunctions.h"
#include "common/timecodefunctions.h"
#include "ffmpegcommon.h"
#include "render/pixelservice.h"
FFmpegDecoder::FFmpegDecoder() :
fmt_ctx_(nullptr),
codec_ctx_(nullptr),
opts_(nullptr),
scale_ctx_(nullptr)
{
}
FFmpegDecoder::~FFmpegDecoder()
{
Close();
}
bool FFmpegDecoder::Open()
{
if (open_) {
return true;
}
int error_code;
// Convert QString to a C string
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 = FFmpegCommon::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
}
// All allocation succeeded so we set the state to open
open_ = true;
return true;
}
FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode)
{
if (!open_ && !Open()) {
return nullptr;
}
if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
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;
}
QFile compressed_frame(GetIndexFilename().append(QString::number(target_ts)));
if (compressed_frame.open(QFile::ReadOnly)) {
QByteArray frame_loader = qUncompress(compressed_frame.readAll());
AVFrame* frame = av_frame_alloc();
if (frame == nullptr) {
qWarning() << "Failed to create AVFrame for swscale";
return nullptr;
}
frame->width = avstream_->codecpar->width;
frame->height = avstream_->codecpar->height;
QDataStream ds(&frame_loader, QIODevice::ReadOnly);
ds >> frame->format;
if (av_frame_get_buffer(frame, 0) != 0) {
qWarning() << "Failed to get AVFrame buffer";
av_frame_free(&frame);
return nullptr;
}
// Read data
size_t pos = sizeof(int);
for (int i=0;i<AV_NUM_DATA_POINTERS;i++) {
size_t plane_size = static_cast<size_t>(frame->linesize[i] * CalculatePlaneHeight(frame->height, static_cast<AVPixelFormat>(frame->format), i));
memcpy(frame->data[i], frame_loader.data() + pos, plane_size);
pos += plane_size;
}
// 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(olive::timestamp_to_time(target_ts, avstream_->time_base));
frame_container->allocate();
// Convert pixel format/linesize if necessary
uint8_t* dst_data = reinterpret_cast<uint8_t*>(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);
av_frame_free(&frame);
return frame_container;
}
return nullptr;
}
FramePtr FFmpegDecoder::RetrieveAudio(const rational &timecode, const rational &length, const AudioRenderingParams &params)
{
if (!open_ && !Open()) {
return nullptr;
}
if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
return nullptr;
}
if (!LoadIndex()) {
Index();
}
Conform(params);
WaveInput input(GetConformedFilename(params));
if (input.open()) {
const AudioRenderingParams& params = input.params();
FramePtr audio_frame = Frame::Create();
audio_frame->set_audio_params(params);
audio_frame->set_sample_count(params.time_to_samples(length));
audio_frame->allocate();
input.read(params.time_to_bytes(timecode),
audio_frame->data(),
audio_frame->allocated_size());
input.close();
return audio_frame;
}
return nullptr;
}
void FFmpegDecoder::Close()
{
frame_index_.clear();
if (scale_ctx_ != nullptr) {
sws_freeContext(scale_ctx_);
scale_ctx_ = 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;
}
void FFmpegDecoder::Conform(const AudioRenderingParams &params)
{
if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
// Nothing to be done
return;
}
if (!LoadIndex()) {
Index();
}
// Get indexed WAV file
WaveInput input(GetIndexFilename());
if (input.open()) {
// If the parameters are equal, nothing to be done
// FIXME: Technically we only need to conform if the SAMPLE RATE is not equal. Format and channel layout conversion
// could be done on the fly so we could perhaps conform less often at some point.
if (input.params() == params) {
input.close();
return;
}
// Otherwise, let's start converting the format
// Generate destination filename for this conversion to see if it exists
QString conformed_fn = GetConformedFilename(params);
if (QFileInfo::exists(conformed_fn)) {
// We must have already conformed this format
input.close();
return;
}
// Set up resampler
SwrContext* resampler = swr_alloc_set_opts(nullptr,
static_cast<int64_t>(params.channel_layout()),
FFmpegCommon::GetFFmpegSampleFormat(params.format()),
params.sample_rate(),
static_cast<int64_t>(input.params().channel_layout()),
FFmpegCommon::GetFFmpegSampleFormat(input.params().format()),
input.params().sample_rate(),
0,
nullptr);
swr_init(resampler);
WaveOutput conformed_output(conformed_fn, params);
if (!conformed_output.open()) {
qWarning() << "Failed to open conformed output:" << conformed_fn;
input.close();
return;
}
// Convert one second of audio at a time
int input_buffer_sz = input.params().time_to_bytes(1);
while (!input.at_end()) {
// Read up to one second of audio from WAV file
QByteArray read_samples = input.read(input_buffer_sz);
// Determine how many samples this is
int in_sample_count = input.params().bytes_to_samples(read_samples.size());
ConformInternal(resampler, &conformed_output, read_samples.data(), in_sample_count);
}
// Flush resampler
ConformInternal(resampler, &conformed_output, nullptr, 0);
// Clean up
swr_free(&resampler);
conformed_output.close();
input.close();
} else {
qWarning() << "Failed to conform file:" << stream()->footage()->filename();
}
}
bool FFmpegDecoder::SupportsVideo()
{
return true;
}
bool FFmpegDecoder::SupportsAudio()
{
return true;
}
void FFmpegDecoder::ConformInternal(SwrContext* resampler, WaveOutput* output, const char* in_data, int in_sample_count)
{
// Determine how many samples the output will be
int out_sample_count = swr_get_out_samples(resampler, in_sample_count);
// Allocate array for the amount of samples we'll need
QByteArray out_samples;
out_samples.resize(output->params().samples_to_bytes(out_sample_count));
char* out_data = out_samples.data();
// Convert samples
int convert_count = swr_convert(resampler,
reinterpret_cast<uint8_t**>(&out_data),
out_sample_count,
reinterpret_cast<const uint8_t**>(&in_data),
in_sample_count);
if (convert_count != out_sample_count) {
out_samples.resize(output->params().samples_to_bytes(convert_count));
}
output->write(out_samples);
}
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;
default:
// We should never realistically get here, but we make an "invalid" stream just in case
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 (!LoadIndex()) {
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;
}
// Allocate a packet and frame for decoding
AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (pkt == nullptr || frame == nullptr) {
// Handle failure to allocate either
Error(QStringLiteral("Failed to allocate resources for indexing"));
} else {
// Reset state
Seek(0);
if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
IndexVideo(pkt, frame);
} else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
IndexAudio(pkt, frame);
}
// Reset state
Seek(0);
}
// Free resources
if (pkt != nullptr)
av_packet_free(&pkt);
if (frame != nullptr)
av_frame_free(&frame);
}
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));
}
QString FFmpegDecoder::GetConformedFilename(const AudioRenderingParams &params)
{
QString index_fn = GetIndexFilename();
WaveInput input(GetIndexFilename());
if (input.open()) {
// If the parameters are equal, nothing to be done
AudioRenderingParams index_params = input.params();
input.close();
if (index_params == params) {
// Source file matches perfectly, no conform required
return index_fn;
}
}
index_fn.append('.');
index_fn.append(QString::number(params.sample_rate()));
index_fn.append('.');
index_fn.append(QString::number(params.format()));
index_fn.append('.');
index_fn.append(QString::number(params.channel_layout()));
return index_fn;
}
bool FFmpegDecoder::LoadIndex()
{
switch (avstream_->codecpar->codec_type) {
case AVMEDIA_TYPE_VIDEO:
{
// 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;
}
break;
}
case AVMEDIA_TYPE_AUDIO:
{
return QFileInfo::exists(GetIndexFilename());
}
default:
break;
}
return false;
}
void FFmpegDecoder::SaveIndex()
{
// 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());
}
}
void FFmpegDecoder::IndexAudio(AVPacket *pkt, AVFrame *frame)
{
// Iterate through each audio frame and extract the PCM data
uint64_t channel_layout = avstream_->codecpar->channel_layout;
if (!channel_layout) {
if (!avstream_->codecpar->channels) {
// No channel data - we can't do anything with this
return;
}
channel_layout = static_cast<uint64_t>(av_get_default_channel_layout(avstream_->codecpar->channels));
}
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);
resampler = swr_alloc_set_opts(nullptr,
static_cast<int64_t>(avstream_->codecpar->channel_layout),
dst_sample_fmt,
avstream_->codecpar->sample_rate,
static_cast<int64_t>(avstream_->codecpar->channel_layout),
src_sample_fmt,
avstream_->codecpar->sample_rate,
0,
nullptr);
} else {
dst_sample_fmt = src_sample_fmt;
}
WaveOutput wave_out(GetIndexFilename(),
AudioRenderingParams(avstream_->codecpar->sample_rate,
channel_layout,
FFmpegCommon::GetNativeSampleFormat(dst_sample_fmt)));
int ret;
if (wave_out.open()) {
while (true) {
ret = GetFrame(pkt, frame);
if (ret < 0) {
break;
} else {
AVFrame* data_frame;
if (resampler != nullptr) {
// We must need to resample this (mainly just convert from planar to packed if necessary)
data_frame = av_frame_alloc();
data_frame->sample_rate = frame->sample_rate;
data_frame->channel_layout = frame->channel_layout;
data_frame->channels = frame->channels;
data_frame->format = dst_sample_fmt;
av_frame_make_writable(data_frame);
int ret = swr_convert_frame(resampler, data_frame, frame);
if (ret != 0) {
char err_str[50];
av_strerror(ret, err_str, 50);
qWarning() << "libswresample failed with error:" << ret << err_str;
}
} else {
// No resampling required, we can write directly from te frame buffer
data_frame = frame;
}
int buffer_sz = av_samples_get_buffer_size(nullptr,
avstream_->codecpar->channels,
data_frame->nb_samples,
dst_sample_fmt,
0); // FIXME: Documentation unclear - should this be 0 or 1?
// Write packed WAV data to the disk cache
wave_out.write(reinterpret_cast<char*>(data_frame->data[0]), buffer_sz);
// If we allocated an output for the resampler, delete it here
if (data_frame != frame) {
av_frame_free(&data_frame);
}
}
}
wave_out.close();
} else {
qWarning() << "Failed to open WAVE output for indexing";
}
if (resampler != nullptr) {
swr_free(&resampler);
}
}
void FFmpegDecoder::IndexVideo(AVPacket* pkt, AVFrame* frame)
{
// 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
int ret;
while (true) {
ret = GetFrame(pkt, frame);
if (ret >= 0) {
// Save frame
QByteArray frame_save;
QDataStream ds(&frame_save, QIODevice::WriteOnly);
ds << frame->format;
// Save data
for (int i=0;i<AV_NUM_DATA_POINTERS;i++) {
frame_save.append(reinterpret_cast<const char*>(frame->data[i]),
frame->linesize[i] * CalculatePlaneHeight(frame->height, static_cast<AVPixelFormat>(frame->format), i));
}
QFile compressed_frame(GetIndexFilename().append(QString::number(frame->pts)));
if (compressed_frame.open(QFile::WriteOnly)) {
compressed_frame.write(qCompress(frame_save));
compressed_frame.close();
}
frame_index_.append(frame->pts);
} else {
// Assume we've reached the end of the file
break;
}
}
// Save index to file
SaveIndex();
}
int FFmpegDecoder::GetFrame(AVPacket *pkt, AVFrame *frame)
{
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;
}
int FFmpegDecoder::CalculatePlaneHeight(int frame_height, const AVPixelFormat &format, int plane)
{
// FIXME: This seems dumb, but I can't find any FFmpeg function that returns this information
if ((plane == 1 || plane == 2)
&& format == AV_PIX_FMT_YUV420P) {
return frame_height/2;
}
return frame_height;
}
int64_t FFmpegDecoder::GetClosestTimestampInIndex(const int64_t &ts)
{
// Index now if we haven't already
if (frame_index_.isEmpty() && !LoadIndex()) {
Index();
}
if (frame_index_.isEmpty()) {
return -1;
}
if (ts <= 0) {
return frame_index_.first();
}
// Use index to find closest frame in file
for (int i=1;i<frame_index_.size();i++) {
int64_t this_ts = frame_index_.at(i);
if (this_ts == ts) {
return ts;
} else if (this_ts > 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);
}
+156
View File
@@ -0,0 +1,156 @@
/***
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/>.
***/
#ifndef FFMPEGDECODER_H
#define FFMPEGDECODER_H
extern "C" {
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}
#include <QVector>
#include "audio/sampleformat.h"
#include "codec/decoder.h"
#include "codec/waveoutput.h"
/**
* @brief A Decoder derivative that wraps FFmpeg functions as on Olive decoder
*/
class FFmpegDecoder : public Decoder
{
public:
// Constructor
FFmpegDecoder();
// Destructor
virtual ~FFmpegDecoder() override;
virtual bool Probe(Footage *f) override;
virtual bool Open() override;
virtual FramePtr RetrieveVideo(const rational &timecode) override;
virtual FramePtr RetrieveAudio(const rational &timecode, const rational &length, const AudioRenderingParams& params) override;
virtual void Close() override;
virtual QString id() override;
virtual int64_t GetTimestampFromTime(const rational& time) override;
virtual void Conform(const AudioRenderingParams& params) override;
virtual bool SupportsVideo() override;
virtual bool SupportsAudio() override;
private:
void ConformInternal(SwrContext *resampler, WaveOutput *output, const char *in_data, int in_sample_count);
/**
* @brief Handle an error
*
* Immediately closes the Decoder (freeing memory resources) and sends the string provided to the warning stream.
* As this function closes the Decoder, no further Decoder functions should be performed after this is called
* (unless the Decoder is opened again first).
*/
void Error(const QString& s);
/**
* @brief Handle an FFmpeg error code
*
* Uses the FFmpeg API to retrieve a descriptive string for this error code and sends it to Error(). As such, this
* function also automatically closes the Decoder.
*
* @param error_code
*/
void FFmpegError(int error_code);
/**
* @brief Uses the FFmpeg API to retrieve a packet (stored in pkt_) and decode it (stored in frame_)
*
* @return
*
* An FFmpeg error code, or >= 0 on success
*/
int GetFrame(AVPacket* pkt, AVFrame* frame);
/**
* @brief Create an index for this media
*
* Indexes are used to improve speed and reliability of imported media. Calling Retrieve() will automatically check
* for an index and create one if it doesn't exist.
*
* Indexing is slow so it's recommended to do it in a background thread. Index() must be called while the Decoder is
* open, and does not automatically call Open() and Close() the Decoder. The caller must call thse manually.
*
* FIXME: This should perhaps become a common function for the base Decoder class
*/
void Index();
/**
* @brief Returns the filename for the index
*
* Retrieves the absolute filename of the index file for this stream. Decoder must be open for this to work correctly.
*/
QString GetIndexFilename();
/**
* @brief Get the destination filename of an audio stream conformed to a set of parameters
*/
QString GetConformedFilename(const AudioRenderingParams &params);
/**
* @brief Used internally to load a frame index into frame_index_
*
* @return
*
* TRUE if a frame index was successfully loaded. FALSE usually means the file didn't exist and Index() should be
* run to create it.
*/
bool LoadIndex();
/**
* @brief Used in Index() to save the just created frame index to a file that can be loaded later
*/
void SaveIndex();
void IndexAudio(AVPacket* pkt, AVFrame* frame);
void IndexVideo(AVPacket* pkt, AVFrame* frame);
int64_t GetClosestTimestampInIndex(const int64_t& ts);
void Seek(int64_t timestamp);
int CalculatePlaneHeight(int frame_height, const AVPixelFormat& format, int plane);
AVFormatContext* fmt_ctx_;
AVCodecContext* codec_ctx_;
AVStream* avstream_;
AVDictionary* opts_;
SwsContext* scale_ctx_;
int output_fmt_;
QVector<int64_t> frame_index_;
};
#endif // FFMPEGDECODER_H
+357
View File
@@ -0,0 +1,357 @@
#include "ffmpegencoder.h"
#include "ffmpegcommon.h"
#include "render/pixelservice.h"
FFmpegEncoder::FFmpegEncoder(const EncodingParams &params) :
Encoder(params),
fmt_ctx_(nullptr),
video_stream_(nullptr),
video_codec_ctx_(nullptr),
video_scale_ctx_(nullptr),
audio_stream_(nullptr),
audio_codec_ctx_(nullptr),
audio_resample_ctx_(nullptr)
{
}
bool FFmpegEncoder::Open()
{
if (open_) {
return true;
}
int error_code;
// Convert QString to C string that FFmpeg expects
QByteArray filename_bytes = params().filename().toUtf8();
const char* filename_c_str = filename_bytes.constData();
// Create output format context
error_code = avformat_alloc_output_context2(&fmt_ctx_, nullptr, nullptr, filename_c_str);
// Check error code
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Initialize a video stream if it's enabled
if (params().video_enabled()) {
if (!InitializeStream(AVMEDIA_TYPE_VIDEO, &video_stream_, &video_codec_ctx_, params().video_codec())) {
return false;
}
// This is the format we will expect frames received in Write() to be in
olive::PixelFormat native_pixel_fmt = params().video_params().format();
// This is the format we will need to convert the frame to for swscale to understand it
video_conversion_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(native_pixel_fmt);
// This is the equivalent pixel format above as an AVPixelFormat that swscale can understand
AVPixelFormat src_pix_fmt = FFmpegCommon::GetFFmpegPixelFormat(video_conversion_fmt_);
// This is the pixel format the encoder wants to encode to
AVPixelFormat encoder_pix_fmt = video_codec_ctx_->pix_fmt;
// Set up a scaling context - if the native pixel format is not equal to the encoder's, we'll need to convert it
// before encoding. Even if we don't, this may be useful for converting between linesizes, etc.
video_scale_ctx_ = sws_getContext(params().video_params().width(),
params().video_params().height(),
src_pix_fmt,
params().video_params().width(),
params().video_params().height(),
encoder_pix_fmt,
0,
nullptr,
nullptr,
nullptr);
}
// Initialize an audio stream if it's enabled
if (params().audio_enabled()
&& !InitializeStream(AVMEDIA_TYPE_AUDIO, &audio_stream_, &audio_codec_ctx_, params().audio_codec())) {
return false;
}
av_dump_format(fmt_ctx_, 0, filename_c_str, 1);
// Open output file for writing
error_code = avio_open(&fmt_ctx_->pb, filename_c_str, AVIO_FLAG_WRITE);
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Write header
error_code = avformat_write_header(fmt_ctx_, nullptr);
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
open_ = true;
return true;
}
void FFmpegEncoder::Write(FramePtr frame)
{
AVFrame* encoded_frame = av_frame_alloc();
AVPacket* pkt = av_packet_alloc();
int error_code;
AVCodecContext* codec_ctx;
AVStream* stream;
if (frame->width() > 0) {
// Frame must be video
encoded_frame->width = frame->width();
encoded_frame->height = frame->height();
encoded_frame->format = video_codec_ctx_->pix_fmt;
error_code = av_frame_get_buffer(encoded_frame, 0);
if (error_code < 0) {
FFmpegError(error_code);
goto fail;
}
// We may need to convert this frame to a frame that swscale will understand
if (frame->format() != video_conversion_fmt_) {
frame = PixelService::ConvertPixelFormat(frame, video_conversion_fmt_);
}
// Use swscale context to convert formats/linesizes
const char* input_data = frame->const_data();
int input_linesize = frame->width() * PixelService::BytesPerPixel(video_conversion_fmt_);
error_code = sws_scale(video_scale_ctx_,
reinterpret_cast<const uint8_t**>(&input_data),
&input_linesize,
0,
frame->height(),
encoded_frame->data,
encoded_frame->linesize);
if (error_code < 0) {
goto fail;
}
codec_ctx = video_codec_ctx_;
stream = video_stream_;
} else {
// Frame must be audio
codec_ctx = audio_codec_ctx_;
stream = audio_stream_;
}
encoded_frame->pts = qRound(frame->timestamp().toDouble() / av_q2d(codec_ctx->time_base));
// Send raw frame to the encoder
error_code = avcodec_send_frame(codec_ctx, encoded_frame);
if (error_code < 0) {
FFmpegError(error_code);
goto fail;
}
// Retrieve packets from encoder
while (error_code >= 0) {
error_code = avcodec_receive_packet(codec_ctx, pkt);
// EAGAIN just means the encoder wants another frame before encoding
if (error_code == AVERROR(EAGAIN)) {
break;
} else if (error_code < 0) {
FFmpegError(error_code);
goto fail;
}
// Set packet stream index
pkt->stream_index = stream->index;
av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
// Write packet to file
av_interleaved_write_frame(fmt_ctx_, pkt);
// Unref packet in case we're getting another
av_packet_unref(pkt);
}
fail:
av_packet_free(&pkt);
av_frame_free(&encoded_frame);
}
void FFmpegEncoder::Close()
{
if (open_) {
// Flush encoders
FlushEncoders();
// We've written a header, so we'll write a trailer
av_write_trailer(fmt_ctx_);
avio_closep(&fmt_ctx_->pb);
}
if (video_scale_ctx_) {
sws_freeContext(video_scale_ctx_);
video_scale_ctx_ = nullptr;
}
if (video_codec_ctx_) {
avcodec_free_context(&video_codec_ctx_);
video_codec_ctx_ = nullptr;
}
if (audio_codec_ctx_) {
avcodec_free_context(&audio_codec_ctx_);
audio_codec_ctx_ = nullptr;
}
if (fmt_ctx_) {
// NOTE: This also frees video_stream_ and audio_stream_
avformat_free_context(fmt_ctx_);
fmt_ctx_ = nullptr;
}
}
void FFmpegEncoder::FFmpegError(int error_code)
{
char err[1024];
av_strerror(error_code, err, 1024);
Error(QStringLiteral("Error encoding %1 - %2 %3").arg(params().filename(),
QString::number(error_code),
err));
}
bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AVCodecContext** codec_ctx_ptr, const QString& codec)
{
if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO) {
Error(QStringLiteral("Cannot initialize a stream that is not a video or audio type"));
return false;
}
// Retrieve codec and convert to C string
QByteArray codec_bytes = codec.toUtf8();
const char* codec_c_str = codec_bytes.constData();
// Find encoder with this name
AVCodec* encoder = avcodec_find_encoder_by_name(codec_c_str);
if (!encoder) {
Error(QStringLiteral("Failed to find codec for %1").arg(codec));
return false;
}
if (encoder->type != type) {
Error(QStringLiteral("Retrieved unexpected codec type %1 for codec %2").arg(QString::number(encoder->type), codec));
return false;
}
if (!InitializeCodecContext(stream_ptr, codec_ctx_ptr, encoder)) {
return false;
}
// Set codec parameters
AVCodecContext* codec_ctx = *codec_ctx_ptr;
AVStream* stream = *stream_ptr;
if (type == AVMEDIA_TYPE_VIDEO) {
codec_ctx->width = params().video_params().width();
codec_ctx->height = params().video_params().height();
codec_ctx->sample_aspect_ratio = {1, 1};
codec_ctx->time_base = params().video_params().time_base().toAVRational();
// FIXME: Make this customizable again
codec_ctx->pix_fmt = encoder->pix_fmts[0];
} else {
codec_ctx->sample_rate = params().audio_params().sample_rate();
codec_ctx->channel_layout = params().audio_params().channel_layout();
codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
codec_ctx->sample_fmt = encoder->sample_fmts[0];
codec_ctx->time_base = {1, codec_ctx->sample_rate};
}
if (!SetupCodecContext(stream, codec_ctx, encoder)) {
return false;
}
return true;
}
bool FFmpegEncoder::InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx, AVCodec* codec)
{
*stream = avformat_new_stream(fmt_ctx_, nullptr);
if (!(*stream)) {
Error(QStringLiteral("Failed to allocate AVStream"));
return false;
}
// Allocate a codec context
*codec_ctx = avcodec_alloc_context3(codec);
if (!(*codec_ctx)) {
Error(QStringLiteral("Failed to allocate AVCodecContext"));
return false;
}
return true;
}
bool FFmpegEncoder::SetupCodecContext(AVStream* stream, AVCodecContext* codec_ctx, AVCodec* codec)
{
int error_code;
if (fmt_ctx_->oformat->flags & AVFMT_GLOBALHEADER) {
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// Try to open encoder
error_code = avcodec_open2(codec_ctx, codec, nullptr);
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
// Copy context settings to codecpar object
error_code = avcodec_parameters_from_context(stream->codecpar, codec_ctx);
if (error_code < 0) {
FFmpegError(error_code);
return false;
}
return true;
}
void FFmpegEncoder::FlushEncoders()
{
if (video_codec_ctx_) {
avcodec_send_frame(video_codec_ctx_, nullptr);
AVPacket* pkt = av_packet_alloc();
int error_code;
do {
error_code = avcodec_receive_packet(video_codec_ctx_, pkt);
if (error_code < 0) {
break;
}
pkt->stream_index = video_stream_->index;
av_packet_rescale_ts(pkt, video_codec_ctx_->time_base, video_stream_->time_base);
av_interleaved_write_frame(fmt_ctx_, pkt);
av_packet_unref(pkt);
} while (error_code >= 0);
av_packet_free(&pkt);
}
}
void FFmpegEncoder::Error(const QString &s)
{
qWarning() << s;
Close();
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef FFMPEGENCODER_H
#define FFMPEGENCODER_H
extern "C" {
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}
#include "codec/encoder.h"
class FFmpegEncoder : public Encoder
{
public:
FFmpegEncoder(const EncodingParams &params);
virtual bool Open() override;
virtual void Write(FramePtr frame) override;
virtual void Close() override;
private:
/**
* @brief Handle an error
*
* Immediately closes the Decoder (freeing memory resources) and sends the string provided to the warning stream.
* As this function closes the Decoder, no further Decoder functions should be performed after this is called
* (unless the Decoder is opened again first).
*/
void Error(const QString& s);
/**
* @brief Handle an FFmpeg error code
*
* Uses the FFmpeg API to retrieve a descriptive string for this error code and sends it to Error(). As such, this
* function also automatically closes the Decoder.
*
* @param error_code
*/
void FFmpegError(int error_code);
bool InitializeStream(enum AVMediaType type, AVStream** stream, AVCodecContext** codec_ctx, const QString& codec);
bool InitializeCodecContext(AVStream** stream, AVCodecContext** codec_ctx, AVCodec* codec);
bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx, AVCodec *codec);
void FlushEncoders();
AVFormatContext* fmt_ctx_;
AVStream* video_stream_;
AVCodecContext* video_codec_ctx_;
SwsContext* video_scale_ctx_;
olive::PixelFormat video_conversion_fmt_;
AVStream* audio_stream_;
AVCodecContext* audio_codec_ctx_;
SwrContext* audio_resample_ctx_;
};
#endif // FFMPEGENCODER_H