merged encoder and decoder code into one folder since they can share quite a bit of code
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# 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/>.
|
||||
|
||||
add_subdirectory(ffmpeg)
|
||||
add_subdirectory(oiio)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
codec/decoder.h
|
||||
codec/decoder.cpp
|
||||
codec/encoder.h
|
||||
codec/encoder.cpp
|
||||
codec/frame.h
|
||||
codec/frame.cpp
|
||||
codec/waveinput.h
|
||||
codec/waveinput.cpp
|
||||
codec/waveoutput.h
|
||||
codec/waveoutput.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
/***
|
||||
|
||||
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 "decoder.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "codec/ffmpeg/ffmpegdecoder.h"
|
||||
#include "codec/oiio/oiiodecoder.h"
|
||||
|
||||
Decoder::Decoder() :
|
||||
open_(false),
|
||||
stream_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
Decoder::Decoder(Stream *fs) :
|
||||
open_(false),
|
||||
stream_(fs)
|
||||
{
|
||||
}
|
||||
|
||||
Decoder::~Decoder()
|
||||
{
|
||||
}
|
||||
|
||||
StreamPtr Decoder::stream()
|
||||
{
|
||||
return stream_;
|
||||
}
|
||||
|
||||
void Decoder::set_stream(StreamPtr fs)
|
||||
{
|
||||
Close();
|
||||
|
||||
stream_ = fs;
|
||||
}
|
||||
|
||||
FramePtr Decoder::RetrieveVideo(const rational &/*timecode*/)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FramePtr Decoder::RetrieveAudio(const rational &/*timecode*/, const rational &/*length*/, const AudioRenderingParams &/*params*/)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Decoder::SupportsVideo()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Decoder::SupportsAudio()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* DECODER STATIC PUBLIC MEMBERS
|
||||
*/
|
||||
|
||||
QVector<DecoderPtr> ReceiveListOfAllDecoders() {
|
||||
QVector<DecoderPtr> decoders;
|
||||
|
||||
// The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last,
|
||||
// since it supports so many formats and we presumably want to override those formats with a more specific decoder.
|
||||
|
||||
decoders.append(std::make_shared<OIIODecoder>());
|
||||
decoders.append(std::make_shared<FFmpegDecoder>());
|
||||
|
||||
return decoders;
|
||||
}
|
||||
|
||||
bool Decoder::ProbeMedia(Footage *f)
|
||||
{
|
||||
// Check for a valid filename
|
||||
if (f->filename().isEmpty()) {
|
||||
qWarning() << "Tried to probe media with an empty filename";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file exists
|
||||
if (!QFileInfo::exists(f->filename())) {
|
||||
qWarning() << "Tried to probe file that doesn't exist:" << f->filename();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset Footage state for probing
|
||||
f->Clear();
|
||||
|
||||
// Create list to iterate through
|
||||
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
|
||||
|
||||
// Pass Footage through each Decoder's probe function
|
||||
for (int i=0;i<decoder_list.size();i++) {
|
||||
|
||||
DecoderPtr decoder = decoder_list.at(i);
|
||||
|
||||
if (decoder->Probe(f)) {
|
||||
|
||||
// We found a Decoder, so we can set this media as valid
|
||||
f->set_status(Footage::kReady);
|
||||
|
||||
// Attach the successful Decoder to this Footage object
|
||||
f->set_decoder(decoder->id());
|
||||
|
||||
// FIXME: Cache the results so we don't have to probe if this media is added a second time
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We aren't able to use this Footage
|
||||
f->set_status(Footage::kInvalid);
|
||||
f->set_decoder(QString());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
DecoderPtr Decoder::CreateFromID(const QString &id)
|
||||
{
|
||||
if (id.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create list to iterate through
|
||||
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
|
||||
|
||||
foreach (DecoderPtr d, decoder_list) {
|
||||
if (d->id() == id) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Decoder::Conform(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
qCritical() << "Conform called on an audio decoder that does not have a handler for it:" << id();
|
||||
abort();
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/***
|
||||
|
||||
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 DECODER_H
|
||||
#define DECODER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/constructors.h"
|
||||
#include "common/rational.h"
|
||||
#include "project/item/footage/footage.h"
|
||||
|
||||
class Decoder;
|
||||
using DecoderPtr = std::shared_ptr<Decoder>;
|
||||
|
||||
/**
|
||||
* @brief A decoder's is the main class for bringing external media into Olive
|
||||
*
|
||||
* Its responsibilities are to serve as
|
||||
* abstraction from codecs/decoders and provide complete frames. These frames can be video or audio data and are
|
||||
* provided as Frame objects in shared pointers to alleviate the responsibility of memory handling.
|
||||
*
|
||||
* The main function in a decoder is Retrieve() which should return complete image/audio data. A decoder should
|
||||
* alleviate all the complexities of codec compression from the rest of the application (i.e. a decoder should never
|
||||
* return a partial frame or require other parts of the system to interface directly with the codec). Often this will
|
||||
* necessitate pre-emptively caching, indexing, or even fully transcoding media before using it which can be implemented
|
||||
* through the Analyze() function.
|
||||
*
|
||||
* A decoder does NOT perform any pixel/sample format conversion. Frames should pass through the PixelFormatConverter
|
||||
* (olive::pix_fmt_conv) to be utilized in the rest of the rendering pipeline.
|
||||
*/
|
||||
class Decoder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Decoder();
|
||||
|
||||
Decoder(Stream* fs);
|
||||
|
||||
// Necessary for subclassing, it's empty
|
||||
virtual ~Decoder() override;
|
||||
|
||||
DISABLE_COPY_MOVE(Decoder)
|
||||
|
||||
virtual QString id() = 0;
|
||||
|
||||
StreamPtr stream();
|
||||
void set_stream(StreamPtr fs);
|
||||
|
||||
/**
|
||||
* @brief Probe a footage file and dump metadata about it
|
||||
*
|
||||
* When a Footage file is imported, we'll need to know whether Olive is equipped with a decoder for utilizing it
|
||||
* and metadata should be retrieved about it if so. For this purpose, the Footage object is passed through all
|
||||
* Probe() functions of available deocders until one returns TRUE. A FALSE return means the Decoder was unable to
|
||||
* parse this file and the next should be tried.
|
||||
*
|
||||
* Probe() differs from Open() since it focuses on a file as a whole rather than one particular stream. Probe()
|
||||
* should be able to be run directly without calling Open() or Close() and should free its memory before returning.
|
||||
*
|
||||
* Probe() will never be called on an object that is also used for decoding. In other words, it will never be called
|
||||
* alongside Open() or Close() externally, so Probe() can use variables that would otherwise be used for decoding
|
||||
* without conflict.
|
||||
*
|
||||
* @param f
|
||||
*
|
||||
* A Footage object to probe. The Footage object will have a valid filename and will be empty prior to being sent
|
||||
* to this function (i.e. Footage::Clear() will not have to be called).
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* TRUE if the Decoder was able to decode this file. FALSE if not. This function should have filled the Footage
|
||||
* object with metadata if it returns TRUE. Otherwise, the Footage object should be untouched.
|
||||
*/
|
||||
virtual bool Probe(Footage* f) = 0;
|
||||
|
||||
/**
|
||||
* @brief Open media/allocate memory
|
||||
*
|
||||
* Any file handles or memory allocation that needs to be done before this instance of a Decoder can return data
|
||||
* should be done here.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* TRUE if successful and ready to return data, FALSE if failed to open and unable to retrieve data. If the function
|
||||
* fails, any memory allocated should be free'd before returning FALSE, possibly by calling Close().
|
||||
*/
|
||||
virtual bool Open() = 0;
|
||||
|
||||
/**
|
||||
* @brief Retrieve video frame
|
||||
*
|
||||
* The main function for retrieving video data from the Decoder. This function should always provide complete frame
|
||||
* data (i.e. no partial frames) at the timecode provided. The Decoder should perform any steps required to retrieve
|
||||
* a complete frame separate from the rest of the program, using any form of caching/indexing to keep this as
|
||||
* performant as possible.
|
||||
*
|
||||
* It's acceptable for this function to check whether the Decoder is open, and call Open() if not. If Open() returns
|
||||
* false, this function should return nullptr.
|
||||
*
|
||||
* @param timecode
|
||||
*
|
||||
* The timecode (a rational in seconds) to retrieve the frame at. If there is not a frame at this precise location
|
||||
* this should be corrected internally to the closest fit for the timecode.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* A FramePtr of valid data at this timecode or nullptr if there was nothing to retrieve at the provided timecode or
|
||||
* the media could not be opened.
|
||||
*/
|
||||
virtual FramePtr RetrieveVideo(const rational& timecode);
|
||||
|
||||
/**
|
||||
* @brief Retrieve video frame
|
||||
*
|
||||
* The main function for retrieving audio data from the Decoder. This function should always provide complete frame
|
||||
* data (i.e. no missing samples) at the timecode and length requested. The Decoder should perform any steps
|
||||
* required to retrieve a complete frame separate from the rest of the program, using any form of caching/indexing
|
||||
* to keep this as performant as possible.
|
||||
*
|
||||
* It's acceptable for this function to check whether the Decoder is open, and call Open() if not. If Open() returns
|
||||
* false, this function should return nullptr.
|
||||
*
|
||||
* @param timecode
|
||||
*
|
||||
* The starting timecode (a rational in seconds) to retrieve the data at.
|
||||
*
|
||||
* @param length
|
||||
*
|
||||
* The total length of audio data to retrieve (a rational in seconds).
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* A FramePtr of valid data at this timecode of the requested length or nullptr if there was nothing to retrieve at
|
||||
* the provided timecode or the media could not be opened.
|
||||
*/
|
||||
virtual FramePtr RetrieveAudio(const rational& timecode, const rational& length, const AudioRenderingParams& params);
|
||||
|
||||
virtual bool SupportsVideo();
|
||||
virtual bool SupportsAudio();
|
||||
|
||||
/**
|
||||
* @brief Close media/deallocate memory
|
||||
*
|
||||
* Any file handles or memory allocations opened in Open() should be cleaned up here.
|
||||
*
|
||||
* As the main memory freeing function, it's good practice to call this in Open() if there's an error that prevents
|
||||
* correct function before Open() returns. As such, Close() should be prepared for not all memory/file handles to
|
||||
* have been opened successfully.
|
||||
*/
|
||||
virtual void Close() = 0;
|
||||
|
||||
/**
|
||||
* @brief Get a media file's internal timestamp
|
||||
*
|
||||
* Used to determine which frame will be served at a given time, useful for caching.
|
||||
*/
|
||||
virtual int64_t GetTimestampFromTime(const rational& time) = 0;
|
||||
|
||||
/**
|
||||
* @brief Try to probe a Footage file by passing it through all available Decoders
|
||||
*
|
||||
* This is a helper function designed to abstract the process of communicating with several Decoders from the rest of
|
||||
* the application. This function will take a Footage file and manually pass it through the available Decoders' Probe()
|
||||
* functions until one indicates that it can decode this file. That Decoder will then dump information about the file
|
||||
* into the Footage object for use throughout the program.
|
||||
*
|
||||
* Probing may be a lengthy process and it's recommended to run this in a separate thread.
|
||||
*
|
||||
* @param f
|
||||
*
|
||||
* A Footage object with a valid filename. If the Footage does not have a valid filename (e.g. is empty or file doesn't
|
||||
* exist), this function will return FALSE.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* TRUE if a Decoder was successfully able to parse and probe this file. FALSE if not.
|
||||
*/
|
||||
static bool ProbeMedia(Footage* f);
|
||||
|
||||
/**
|
||||
* @brief Create a Decoder instance using a Decoder ID
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* A Decoder instance or nullptr if a Decoder with this ID does not exist
|
||||
*/
|
||||
static DecoderPtr CreateFromID(const QString& id);
|
||||
|
||||
/**
|
||||
* @brief Conform an audio stream to match certain parameters (audio only)
|
||||
*
|
||||
* Resamples and converts the currently open audio to match the params. If the audio doesn't need conforming (e.g.
|
||||
* audio params already match or a conformed match already exists), this function will return immediately. Otherwise
|
||||
* it will block the calling thread until the conform is complete. This function should therefore only be called
|
||||
* from a background render thread.
|
||||
*
|
||||
* All audio decoders must override this. It's not pure since video decoders don't need to use this, but default
|
||||
* behavior will abort since it should never be called.
|
||||
*/
|
||||
virtual void Conform(const AudioRenderingParams& params);
|
||||
|
||||
protected:
|
||||
bool open_;
|
||||
|
||||
private:
|
||||
StreamPtr stream_;
|
||||
};
|
||||
|
||||
#endif // DECODER_H
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "encoder.h"
|
||||
|
||||
#include "ffmpeg/ffmpegencoder.h"
|
||||
|
||||
Encoder::Encoder(const EncodingParams ¶ms) :
|
||||
open_(false),
|
||||
params_(params)
|
||||
{
|
||||
}
|
||||
|
||||
const EncodingParams &Encoder::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
EncodingParams::EncodingParams() :
|
||||
video_enabled_(false),
|
||||
audio_enabled_(false)
|
||||
{
|
||||
}
|
||||
|
||||
void EncodingParams::SetFilename(const QString &filename)
|
||||
{
|
||||
filename_ = filename;
|
||||
}
|
||||
|
||||
void EncodingParams::EnableVideo(const VideoRenderingParams &video_params, const QString &vcodec)
|
||||
{
|
||||
video_enabled_ = true;
|
||||
video_params_ = video_params;
|
||||
video_codec_ = vcodec;
|
||||
}
|
||||
|
||||
void EncodingParams::EnableAudio(const AudioRenderingParams &audio_params, const QString &acodec)
|
||||
{
|
||||
audio_enabled_ = true;
|
||||
audio_params_ = audio_params;
|
||||
audio_codec_ = acodec;
|
||||
}
|
||||
|
||||
const QString &EncodingParams::filename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
|
||||
bool EncodingParams::video_enabled() const
|
||||
{
|
||||
return video_enabled_;
|
||||
}
|
||||
|
||||
const QString &EncodingParams::video_codec() const
|
||||
{
|
||||
return video_codec_;
|
||||
}
|
||||
|
||||
const VideoRenderingParams &EncodingParams::video_params() const
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
bool EncodingParams::audio_enabled() const
|
||||
{
|
||||
return audio_enabled_;
|
||||
}
|
||||
|
||||
const QString &EncodingParams::audio_codec() const
|
||||
{
|
||||
return audio_codec_;
|
||||
}
|
||||
|
||||
const AudioRenderingParams &EncodingParams::audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
EncoderPtr Encoder::CreateFromID(const QString &id, const EncodingParams& params)
|
||||
{
|
||||
return std::make_shared<FFmpegEncoder>(params);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef ENCODER_H
|
||||
#define ENCODER_H
|
||||
|
||||
#include <memory>
|
||||
#include <QString>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "common/constructors.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
class Encoder;
|
||||
using EncoderPtr = std::shared_ptr<Encoder>;
|
||||
|
||||
class EncodingParams {
|
||||
public:
|
||||
EncodingParams();
|
||||
|
||||
void SetFilename(const QString& filename);
|
||||
void EnableVideo(const VideoRenderingParams& video_params, const QString& vcodec);
|
||||
void EnableAudio(const AudioRenderingParams& audio_params, const QString& acodec);
|
||||
|
||||
const QString& filename() const;
|
||||
|
||||
bool video_enabled() const;
|
||||
const QString& video_codec() const;
|
||||
const VideoRenderingParams& video_params() const;
|
||||
|
||||
bool audio_enabled() const;
|
||||
const QString& audio_codec() const;
|
||||
const AudioRenderingParams& audio_params() const;
|
||||
|
||||
private:
|
||||
QString filename_;
|
||||
|
||||
bool video_enabled_;
|
||||
QString video_codec_;
|
||||
VideoRenderingParams video_params_;
|
||||
|
||||
bool audio_enabled_;
|
||||
QString audio_codec_;
|
||||
AudioRenderingParams audio_params_;
|
||||
};
|
||||
|
||||
class Encoder
|
||||
{
|
||||
public:
|
||||
Encoder(const EncodingParams& params);
|
||||
|
||||
DISABLE_COPY_MOVE(Encoder)
|
||||
|
||||
virtual bool Open() = 0;
|
||||
virtual void Write(FramePtr frame) = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
/**
|
||||
* @brief Create a Encoder instance using a Encoder ID
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* A Encoder instance or nullptr if a Decoder with this ID does not exist
|
||||
*/
|
||||
static EncoderPtr CreateFromID(const QString& id, const EncodingParams ¶ms);
|
||||
|
||||
protected:
|
||||
const EncodingParams& params() const;
|
||||
|
||||
bool open_;
|
||||
|
||||
private:
|
||||
EncodingParams params_;
|
||||
};
|
||||
|
||||
#endif // ENCODER_H
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -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 ¶ms);
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -0,0 +1,357 @@
|
||||
#include "ffmpegencoder.h"
|
||||
|
||||
#include "ffmpegcommon.h"
|
||||
#include "render/pixelservice.h"
|
||||
|
||||
FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) :
|
||||
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();
|
||||
}
|
||||
@@ -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 ¶ms);
|
||||
|
||||
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
|
||||
@@ -0,0 +1,145 @@
|
||||
/***
|
||||
|
||||
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 "frame.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "render/pixelservice.h"
|
||||
|
||||
Frame::Frame() :
|
||||
width_(0),
|
||||
height_(0),
|
||||
format_(olive::PIX_FMT_INVALID),
|
||||
sample_count_(0),
|
||||
timestamp_(0)
|
||||
{
|
||||
}
|
||||
|
||||
FramePtr Frame::Create()
|
||||
{
|
||||
return std::make_shared<Frame>();
|
||||
}
|
||||
|
||||
const int &Frame::width()
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
|
||||
void Frame::set_width(const int &width)
|
||||
{
|
||||
width_ = width;
|
||||
}
|
||||
|
||||
const int &Frame::height()
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
void Frame::set_height(const int &height)
|
||||
{
|
||||
height_ = height;
|
||||
}
|
||||
|
||||
const AudioRenderingParams &Frame::audio_params()
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void Frame::set_audio_params(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
audio_params_ = params;
|
||||
}
|
||||
|
||||
const rational &Frame::timestamp()
|
||||
{
|
||||
return timestamp_;
|
||||
}
|
||||
|
||||
void Frame::set_timestamp(const rational ×tamp)
|
||||
{
|
||||
timestamp_ = timestamp;
|
||||
}
|
||||
|
||||
/*const int64_t &Frame::native_timestamp()
|
||||
{
|
||||
return native_timestamp_;
|
||||
}
|
||||
|
||||
void Frame::set_native_timestamp(const int64_t ×tamp)
|
||||
{
|
||||
native_timestamp_ = timestamp;
|
||||
}*/
|
||||
|
||||
const olive::PixelFormat &Frame::format()
|
||||
{
|
||||
return format_;
|
||||
}
|
||||
|
||||
void Frame::set_format(const olive::PixelFormat &format)
|
||||
{
|
||||
format_ = format;
|
||||
}
|
||||
|
||||
QByteArray Frame::ToByteArray()
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
const int &Frame::sample_count()
|
||||
{
|
||||
return sample_count_;
|
||||
}
|
||||
|
||||
void Frame::set_sample_count(const int &audio_sample_count)
|
||||
{
|
||||
sample_count_ = audio_sample_count;
|
||||
}
|
||||
|
||||
char *Frame::data()
|
||||
{
|
||||
return data_.data();
|
||||
}
|
||||
|
||||
const char *Frame::const_data()
|
||||
{
|
||||
return data_.constData();
|
||||
}
|
||||
|
||||
void Frame::allocate()
|
||||
{
|
||||
// Assume this frame is intended to be a video frame
|
||||
if (width_ > 0 && height_ > 0) {
|
||||
data_.resize(PixelService::GetBufferSize(static_cast<olive::PixelFormat>(format_), width_, height_));
|
||||
} else if (sample_count_ > 0) {
|
||||
data_.resize(audio_params_.samples_to_bytes(sample_count_));
|
||||
}
|
||||
}
|
||||
|
||||
void Frame::destroy()
|
||||
{
|
||||
data_.clear();
|
||||
}
|
||||
|
||||
int Frame::allocated_size() const
|
||||
{
|
||||
return data_.size();
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/***
|
||||
|
||||
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 FRAME_H
|
||||
#define FRAME_H
|
||||
|
||||
#include <memory>
|
||||
#include <QVector>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/pixelformat.h"
|
||||
|
||||
class Frame;
|
||||
using FramePtr = std::shared_ptr<Frame>;
|
||||
|
||||
/**
|
||||
* @brief Video frame data or audio sample data from a Decoder
|
||||
*
|
||||
* Abstraction from AVFrame. Currently a simple AVFrame wrapper.
|
||||
*
|
||||
* This class does not support copying at this time.
|
||||
*/
|
||||
class Frame
|
||||
{
|
||||
public:
|
||||
/// Normal constructor
|
||||
Frame();
|
||||
|
||||
static FramePtr Create();
|
||||
|
||||
/**
|
||||
* @brief Get frame's width in pixels
|
||||
*/
|
||||
const int& width();
|
||||
void set_width(const int& width);
|
||||
|
||||
/**
|
||||
* @brief Get frame's height in pixels
|
||||
*/
|
||||
const int& height();
|
||||
void set_height(const int& height);
|
||||
|
||||
const AudioRenderingParams& audio_params();
|
||||
void set_audio_params(const AudioRenderingParams& params);
|
||||
|
||||
const int &sample_count();
|
||||
void set_sample_count(const int &sample_count);
|
||||
|
||||
/**
|
||||
* @brief Get frame's timestamp.
|
||||
*
|
||||
* This timestamp is always a rational that will equate to the time in seconds.
|
||||
*/
|
||||
const rational& timestamp();
|
||||
void set_timestamp(const rational& timestamp);
|
||||
|
||||
/*const int64_t& native_timestamp();
|
||||
void set_native_timestamp(const int64_t& timestamp);*/
|
||||
|
||||
/**
|
||||
* @brief Get frame's format
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Currently this will either be an olive::PixelFormat (video) or an olive::SampleFormat (audio).
|
||||
*/
|
||||
const olive::PixelFormat& format();
|
||||
void set_format(const olive::PixelFormat& format);
|
||||
|
||||
/**
|
||||
* @brief Returns a copy of the data in this frame as a QByteArray
|
||||
*
|
||||
* Will always do a deep copy. If you want to affect the data directly, use data() instead.
|
||||
*/
|
||||
QByteArray ToByteArray();
|
||||
|
||||
/**
|
||||
* @brief Get the data buffer of this frame
|
||||
*/
|
||||
char* data();
|
||||
|
||||
/**
|
||||
* @brief Get the const data buffer of this frame
|
||||
*/
|
||||
const char* const_data();
|
||||
|
||||
/**
|
||||
* @brief Allocate memory buffer to store data based on parameters
|
||||
*
|
||||
* For video frames, the width(), height(), and format() must be set for this function to work.
|
||||
*
|
||||
* If a memory buffer has been previously allocated without destroying, this function will destroy it.
|
||||
*/
|
||||
void allocate();
|
||||
|
||||
/**
|
||||
* @brief Destroy a memory buffer allocated with allocate()
|
||||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* @brief Returns the size of the array returned in data() in bytes
|
||||
*
|
||||
* Returns 0 if nothing is allocated.
|
||||
*/
|
||||
int allocated_size() const;
|
||||
|
||||
private:
|
||||
int width_;
|
||||
|
||||
int height_;
|
||||
|
||||
olive::PixelFormat format_;
|
||||
|
||||
AudioRenderingParams audio_params_;
|
||||
|
||||
int sample_count_;
|
||||
|
||||
QByteArray data_;
|
||||
|
||||
rational timestamp_;
|
||||
|
||||
};
|
||||
|
||||
#endif // FRAME_H
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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/oiio/oiiodecoder.h
|
||||
codec/oiio/oiiodecoder.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
/***
|
||||
|
||||
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 "oiiodecoder.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "common/define.h"
|
||||
|
||||
OIIODecoder::OIIODecoder() :
|
||||
image_(nullptr),
|
||||
frame_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
QString OIIODecoder::id()
|
||||
{
|
||||
return "oiio";
|
||||
}
|
||||
|
||||
bool OIIODecoder::Probe(Footage *f)
|
||||
{
|
||||
auto in = OIIO::ImageInput::open(f->filename().toStdString());
|
||||
|
||||
if (!in) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!strcmp(in->format_name(), "FFmpeg movie")) {
|
||||
// If this is FFmpeg via OIIO, fall-through to our native FFmpeg decoder
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get stats for this image and dump them into the Footage file
|
||||
const OIIO::ImageSpec& spec = in->spec();
|
||||
|
||||
ImageStreamPtr image_stream = std::make_shared<ImageStream>();
|
||||
image_stream->set_width(spec.width);
|
||||
image_stream->set_height(spec.height);
|
||||
|
||||
// OIIO automatically premultiplies alpha
|
||||
// FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this likely reduces the
|
||||
// fidelity?
|
||||
image_stream->set_premultiplied_alpha(true);
|
||||
|
||||
f->add_stream(image_stream);
|
||||
|
||||
// If we're here, we have a successful image open
|
||||
in->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OIIODecoder::Open()
|
||||
{
|
||||
image_ = OIIO::ImageInput::open(stream()->footage()->filename().toStdString());
|
||||
|
||||
if (!image_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we can work with this pixel format
|
||||
const OIIO::ImageSpec& spec = image_->spec();
|
||||
|
||||
width_ = spec.width;
|
||||
height_ = spec.height;
|
||||
|
||||
// Weirdly, switch statement doesn't work correctly here
|
||||
if (spec.format == OIIO::TypeDesc::UINT8) {
|
||||
pix_fmt_ = olive::PIX_FMT_RGBA8;
|
||||
} else if (spec.format == OIIO::TypeDesc::UINT16) {
|
||||
pix_fmt_ = olive::PIX_FMT_RGBA16U;
|
||||
} else if (spec.format == OIIO::TypeDesc::HALF) {
|
||||
pix_fmt_ = olive::PIX_FMT_RGBA16F;
|
||||
} else if (spec.format == OIIO::TypeDesc::FLOAT) {
|
||||
pix_fmt_ = olive::PIX_FMT_RGBA32F;
|
||||
} else {
|
||||
qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format";
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: Many OIIO pixel formats are not handled here
|
||||
|
||||
is_rgba_ = (spec.nchannels == kRGBAChannels);
|
||||
|
||||
pix_fmt_info_ = PixelService::GetPixelFormatInfo(static_cast<olive::PixelFormat>(pix_fmt_));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FramePtr OIIODecoder::RetrieveVideo(const rational &timecode)
|
||||
{
|
||||
if (!open_ && !Open()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Q_UNUSED(timecode)
|
||||
|
||||
if (!frame_) {
|
||||
frame_ = Frame::Create();
|
||||
|
||||
frame_->set_width(width_);
|
||||
frame_->set_height(height_);
|
||||
frame_->set_format(pix_fmt_);
|
||||
frame_->allocate();
|
||||
|
||||
// Use the native format to determine what format OIIO should return
|
||||
// FIXME: Behavior of RGB images as opposed to RGBA?
|
||||
image_->read_image(pix_fmt_info_.oiio_desc, frame_->data());
|
||||
|
||||
if (!is_rgba_) {
|
||||
PixelService::ConvertRGBtoRGBA(frame_);
|
||||
}
|
||||
}
|
||||
|
||||
return frame_;
|
||||
}
|
||||
|
||||
void OIIODecoder::Close()
|
||||
{
|
||||
if (image_ != nullptr) {
|
||||
image_->close();
|
||||
image_ = nullptr;
|
||||
}
|
||||
|
||||
frame_ = nullptr;
|
||||
}
|
||||
|
||||
int64_t OIIODecoder::GetTimestampFromTime(const rational &time)
|
||||
{
|
||||
Q_UNUSED(time)
|
||||
|
||||
// A still image will always return the same frame
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool OIIODecoder::SupportsVideo()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/***
|
||||
|
||||
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 OIIODECODER_H
|
||||
#define OIIODECODER_H
|
||||
|
||||
#include <OpenImageIO/imageio.h>
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "render/pixelservice.h"
|
||||
|
||||
class OIIODecoder : public Decoder
|
||||
{
|
||||
public:
|
||||
OIIODecoder();
|
||||
|
||||
virtual QString id() override;
|
||||
|
||||
virtual bool Probe(Footage *f) override;
|
||||
|
||||
virtual bool Open() override;
|
||||
|
||||
virtual FramePtr RetrieveVideo(const rational &timecode) override;
|
||||
|
||||
virtual void Close() override;
|
||||
|
||||
virtual int64_t GetTimestampFromTime(const rational &time) override;
|
||||
|
||||
virtual bool SupportsVideo() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<OIIO::ImageInput> image_;
|
||||
|
||||
int width_;
|
||||
|
||||
int height_;
|
||||
|
||||
olive::PixelFormat pix_fmt_;
|
||||
|
||||
PixelFormatInfo pix_fmt_info_;
|
||||
|
||||
bool is_rgba_;
|
||||
|
||||
FramePtr frame_;
|
||||
|
||||
};
|
||||
|
||||
#endif // OIIODECODER_H
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "waveinput.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
#include <QDataStream>
|
||||
|
||||
WaveInput::WaveInput(const QString &f) :
|
||||
file_(f)
|
||||
{
|
||||
}
|
||||
|
||||
WaveInput::~WaveInput()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool WaveInput::open()
|
||||
{
|
||||
if (!file_.open(QFile::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_.read(4) != "RIFF") {
|
||||
close();
|
||||
qDebug() << "No RIFF found";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip filesize bytes
|
||||
file_.seek(file_.pos() + 4);
|
||||
|
||||
if (file_.read(4) != "WAVE") {
|
||||
close();
|
||||
qDebug() << "No WAVE found";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find fmt_ section
|
||||
if (!find_str(&file_, "fmt ")) {
|
||||
close();
|
||||
qDebug() << "No fmt found";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip fmt_ section size
|
||||
file_.seek(file_.pos()+4);
|
||||
|
||||
// Create data stream for reading bytes into types
|
||||
QDataStream data_stream(&file_);
|
||||
data_stream.setByteOrder(QDataStream::LittleEndian);
|
||||
|
||||
// Read data type
|
||||
uint16_t data_type;
|
||||
data_stream >> data_type;
|
||||
|
||||
bool data_is_float;
|
||||
switch (data_type) {
|
||||
case 1: // PCM Integer
|
||||
data_is_float = false;
|
||||
break;
|
||||
case 3:
|
||||
data_is_float = true;
|
||||
break;
|
||||
default:
|
||||
// If it's neither float nor int, we can't work with this file
|
||||
close();
|
||||
qDebug() << "Invalid WAV type" << data_type;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read number of channels
|
||||
uint16_t channel_count;
|
||||
data_stream >> channel_count;
|
||||
|
||||
uint64_t channel_layout = static_cast<uint64_t>(av_get_default_channel_layout(channel_count));
|
||||
|
||||
int32_t sample_rate;
|
||||
data_stream >> sample_rate;
|
||||
|
||||
// Skip bytes per second value and bytes per sample value
|
||||
file_.seek(file_.pos() + 6);
|
||||
|
||||
uint16_t bits_per_sample;
|
||||
data_stream >> bits_per_sample;
|
||||
|
||||
SampleFormat format;
|
||||
|
||||
switch (bits_per_sample) {
|
||||
case 8:
|
||||
format = SAMPLE_FMT_U8;
|
||||
break;
|
||||
case 16:
|
||||
format = SAMPLE_FMT_S16;
|
||||
break;
|
||||
case 32:
|
||||
if (data_is_float) {
|
||||
format = SAMPLE_FMT_FLT;
|
||||
} else {
|
||||
format = SAMPLE_FMT_S32;
|
||||
}
|
||||
break;
|
||||
case 64:
|
||||
if (data_is_float) {
|
||||
format = SAMPLE_FMT_DBL;
|
||||
} else {
|
||||
format = SAMPLE_FMT_S64;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// We don't know this format...
|
||||
close();
|
||||
qDebug() << "Invalid format found" << bits_per_sample;
|
||||
return false;
|
||||
}
|
||||
|
||||
// We're good to go!
|
||||
params_ = AudioRenderingParams(sample_rate, channel_layout, format);
|
||||
|
||||
if (!find_str(&file_, "data")) {
|
||||
close();
|
||||
qDebug() << "No data tag found";
|
||||
return false;
|
||||
}
|
||||
|
||||
data_stream >> data_size_;
|
||||
data_position_ = file_.pos();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WaveInput::is_open() const
|
||||
{
|
||||
return file_.isOpen();
|
||||
}
|
||||
|
||||
QByteArray WaveInput::read(int length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return file_.read(length);
|
||||
}
|
||||
|
||||
QByteArray WaveInput::read(int offset, int length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
file_.seek(offset + data_position_);
|
||||
return file_.read(length);
|
||||
}
|
||||
|
||||
void WaveInput::read(int offset, char *buffer, int length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
file_.seek(offset + data_position_);
|
||||
file_.read(buffer, length);
|
||||
}
|
||||
|
||||
bool WaveInput::at_end() const
|
||||
{
|
||||
return file_.atEnd();
|
||||
}
|
||||
|
||||
const AudioRenderingParams &WaveInput::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void WaveInput::close()
|
||||
{
|
||||
if (file_.isOpen()) {
|
||||
file_.close();
|
||||
}
|
||||
}
|
||||
|
||||
int WaveInput::sample_count() const
|
||||
{
|
||||
return params_.bytes_to_samples(static_cast<int>(data_size_));
|
||||
}
|
||||
|
||||
bool WaveInput::find_str(QFile *f, const char *str)
|
||||
{
|
||||
qint64 pos = f->pos();
|
||||
while (f->read(4) != str) {
|
||||
if (f->atEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pos++;
|
||||
f->seek(pos);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef WAVEINPUT_H
|
||||
#define WAVEINPUT_H
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include "common/constructors.h"
|
||||
#include "render/audioparams.h"
|
||||
|
||||
class WaveInput
|
||||
{
|
||||
public:
|
||||
WaveInput(const QString& f);
|
||||
|
||||
~WaveInput();
|
||||
|
||||
DISABLE_COPY_MOVE(WaveInput)
|
||||
|
||||
bool open();
|
||||
|
||||
bool is_open() const;
|
||||
|
||||
QByteArray read(int length);
|
||||
QByteArray read(int offset, int length);
|
||||
void read(int offset, char *buffer, int length);
|
||||
|
||||
bool at_end() const;
|
||||
|
||||
const AudioRenderingParams& params() const;
|
||||
|
||||
void close();
|
||||
|
||||
int sample_count() const;
|
||||
|
||||
private:
|
||||
bool find_str(QFile* f, const char* str);
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
QFile file_;
|
||||
|
||||
qint64 data_position_;
|
||||
|
||||
quint32 data_size_;
|
||||
};
|
||||
|
||||
#endif // WAVEINPUT_H
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "waveoutput.h"
|
||||
|
||||
const int16_t kWAVIntegerFormat = 1;
|
||||
const int16_t kWAVFloatFormat = 3;
|
||||
|
||||
WaveOutput::WaveOutput(const QString &f,
|
||||
const AudioRenderingParams& params) :
|
||||
file_(f),
|
||||
params_(params)
|
||||
{
|
||||
Q_ASSERT(params_.is_valid());
|
||||
}
|
||||
|
||||
WaveOutput::~WaveOutput()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool WaveOutput::open()
|
||||
{
|
||||
data_length_ = 0;
|
||||
|
||||
if (file_.open(QFile::WriteOnly)) {
|
||||
// RIFF header
|
||||
file_.write("RIFF");
|
||||
|
||||
// Total file size minus RIFF and this integer (minus 8 bytes, filled in later)
|
||||
write_int<int32_t>(&file_, 0);
|
||||
|
||||
// File type header
|
||||
file_.write("WAVE");
|
||||
|
||||
// Begin format descriptor chunk
|
||||
file_.write("fmt ");
|
||||
|
||||
// Format chunk size
|
||||
write_int<int32_t>(&file_, 16);
|
||||
|
||||
// Type of format
|
||||
switch (params_.format()) {
|
||||
case SAMPLE_FMT_U8:
|
||||
case SAMPLE_FMT_S16:
|
||||
case SAMPLE_FMT_S32:
|
||||
case SAMPLE_FMT_S64:
|
||||
write_int<int16_t>(&file_, kWAVIntegerFormat);
|
||||
break;
|
||||
case SAMPLE_FMT_FLT:
|
||||
case SAMPLE_FMT_DBL:
|
||||
write_int<int16_t>(&file_, kWAVFloatFormat);
|
||||
break;
|
||||
case SAMPLE_FMT_INVALID:
|
||||
case SAMPLE_FMT_COUNT:
|
||||
qWarning() << "Invalid sample format for WAVE audio";
|
||||
file_.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Number of channels
|
||||
write_int<int16_t>(&file_, static_cast<int16_t>(params_.channel_count()));
|
||||
|
||||
// Sample rate
|
||||
write_int<int32_t>(&file_, params_.sample_rate());
|
||||
|
||||
// Bytes per second
|
||||
write_int<int32_t>(&file_, params_.samples_to_bytes(params_.sample_rate()));
|
||||
|
||||
// Bytes per sample
|
||||
write_int<int16_t>(&file_, static_cast<int16_t>(params_.samples_to_bytes(1)));
|
||||
|
||||
// Bits per sample per channel
|
||||
write_int<int16_t>(&file_, static_cast<int16_t>(params_.bits_per_sample()));
|
||||
|
||||
// Data chunk header
|
||||
file_.write("data");
|
||||
|
||||
// Size of data chunk (filled in later)
|
||||
write_int<int32_t>(&file_, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void WaveOutput::write(const QByteArray &bytes)
|
||||
{
|
||||
if (file_.isOpen()) {
|
||||
file_.write(bytes);
|
||||
|
||||
data_length_ += bytes.size();
|
||||
}
|
||||
}
|
||||
|
||||
void WaveOutput::write(const char *bytes, int length)
|
||||
{
|
||||
if (file_.isOpen()) {
|
||||
file_.write(bytes, length);
|
||||
|
||||
data_length_ += length;
|
||||
}
|
||||
}
|
||||
|
||||
void WaveOutput::close()
|
||||
{
|
||||
if (file_.isOpen()) {
|
||||
|
||||
// Write file sizes
|
||||
file_.seek(4);
|
||||
write_int<int32_t>(&file_, data_length_ + 36);
|
||||
|
||||
file_.seek(40);
|
||||
write_int<int32_t>(&file_, data_length_);
|
||||
|
||||
file_.close();
|
||||
}
|
||||
}
|
||||
|
||||
const AudioRenderingParams &WaveOutput::params() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void WaveOutput::switch_endianness(QByteArray& array)
|
||||
{
|
||||
int half_sz = array.size()/2;
|
||||
|
||||
for (int i=0;i<half_sz;i++) {
|
||||
int oppose_index = array.size() - i - 1;
|
||||
|
||||
char temp = array[i];
|
||||
array[i] = array[oppose_index];
|
||||
array[oppose_index] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void WaveOutput::write_int(QFile *file, T integer)
|
||||
{
|
||||
QByteArray bytes;
|
||||
bytes.resize(sizeof(T));
|
||||
memcpy(bytes.data(), &integer, static_cast<size_t>(bytes.size()));
|
||||
|
||||
// WAV expects little-endian, so if the integer is big endian we need to switch
|
||||
if (QSysInfo::ByteOrder == QSysInfo::BigEndian) {
|
||||
switch_endianness(bytes);
|
||||
}
|
||||
|
||||
file->write(bytes);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef WAVEAUDIO_H
|
||||
#define WAVEAUDIO_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
|
||||
#include "audio/sampleformat.h"
|
||||
#include "common/constructors.h"
|
||||
#include "render/audioparams.h"
|
||||
|
||||
class WaveOutput
|
||||
{
|
||||
public:
|
||||
WaveOutput(const QString& f,
|
||||
const AudioRenderingParams& params);
|
||||
|
||||
~WaveOutput();
|
||||
|
||||
DISABLE_COPY_MOVE(WaveOutput)
|
||||
|
||||
bool open();
|
||||
|
||||
void write(const QByteArray& bytes);
|
||||
void write(const char* bytes, int length);
|
||||
|
||||
void close();
|
||||
|
||||
const AudioRenderingParams& params() const;
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
void write_int(QFile* file, T integer);
|
||||
|
||||
void switch_endianness(QByteArray &array);
|
||||
|
||||
QFile file_;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
int data_length_;
|
||||
|
||||
};
|
||||
|
||||
#endif // WAVEAUDIO_H
|
||||
Reference in New Issue
Block a user