footage: merge video and image streams and improve image sequence import process

Image streams were initially separated from video streams, but they're now
joined with a parameter defining if they're a still image, image sequence,
or regular video. The image sequence import process has also improved so
that if images from the same sequence are imported too, they'll either be
ignored or the user won't be asked again for those if they should be an
image sequence (fixes #1193). Also shifts decoder "probe" process to return
an item, useful if the decoder returns a non-footage item.
This commit is contained in:
itsmattkc
2020-09-26 19:45:07 +10:00
parent 85abd6c6b4
commit 6db591e89f
37 changed files with 613 additions and 779 deletions
+18 -20
View File
@@ -98,23 +98,20 @@ QVector<DecoderPtr> ReceiveListOfAllDecoders() {
return decoders;
}
bool Decoder::ProbeMedia(Footage *f, const QAtomicInt* cancelled)
ItemPtr Decoder::ProbeMedia(const QString &filename, const QAtomicInt* cancelled)
{
// Check for a valid filename
if (f->filename().isEmpty()) {
if (filename.isEmpty()) {
qWarning() << "Tried to probe media with an empty filename";
return false;
return nullptr;
}
// Check file exists
if (!QFileInfo::exists(f->filename())) {
qWarning() << "Tried to probe file that doesn't exist:" << f->filename();
return false;
if (!QFileInfo::exists(filename)) {
qWarning() << "Tried to probe file that doesn't exist:" << filename;
return nullptr;
}
// Reset Footage state for probing
f->Clear();
// Create list to iterate through
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
@@ -122,30 +119,31 @@ bool Decoder::ProbeMedia(Footage *f, const QAtomicInt* cancelled)
for (int i=0;i<decoder_list.size();i++) {
if (cancelled && *cancelled) {
return false;
return nullptr;
}
DecoderPtr decoder = decoder_list.at(i);
if (decoder->Probe(f, cancelled)) {
ItemPtr item = decoder->Probe(filename, cancelled);
// We found a Decoder, so we can set this media as valid
f->set_status(Footage::kReady);
if (item) {
if (item->type() == Item::kFootage) {
// Attach the successful Decoder to this Footage object
FootagePtr footage = std::static_pointer_cast<Footage>(item);
footage->set_decoder(decoder->id());
footage->SetValid();
}
// 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;
return item;
}
}
// We aren't able to use this Footage
f->set_status(Footage::kInvalid);
f->set_decoder(QString());
return false;
return nullptr;
}
DecoderPtr Decoder::CreateFromID(const QString &id)
+8 -8
View File
@@ -102,7 +102,7 @@ public:
* 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, const QAtomicInt* cancelled) = 0;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const = 0;
/**
* @brief Open media/allocate memory
@@ -199,7 +199,7 @@ public:
*
* TRUE if a Decoder was successfully able to parse and probe this file. FALSE if not.
*/
static bool ProbeMedia(Footage* f, const QAtomicInt *cancelled);
static ItemPtr ProbeMedia(const QString& filename, const QAtomicInt *cancelled);
/**
* @brief Create a Decoder instance using a Decoder ID
@@ -232,6 +232,12 @@ public:
*/
bool HasConformedVersion(const AudioParams& params);
static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number);
static int GetImageSequenceDigitCount(const QString& filename);
static int64_t GetImageSequenceIndex(const QString& filename);
signals:
/**
* @brief While indexing, this signal will provide progress as a percentage (0-100 inclusive) if
@@ -249,12 +255,6 @@ protected:
QString GetIndexFilename();
static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number);
static int GetImageSequenceDigitCount(const QString& filename);
static int64_t GetImageSequenceIndex(const QString& filename);
bool open_;
private:
+29 -37
View File
@@ -85,7 +85,7 @@ bool FFmpegDecoder::Open()
return false;
}
if (stream()->type() == Stream::kImage || stream()->type() == Stream::kVideo) {
if (stream()->type() == Stream::kVideo) {
// Get an Olive compatible AVPixelFormat
src_pix_fmt_ = static_cast<AVPixelFormat>(our_instance->stream()->codecpar->format);
ideal_pix_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(src_pix_fmt_);
@@ -130,12 +130,12 @@ bool FFmpegDecoder::Open()
FramePtr FFmpegDecoder::RetrieveStillImage(const rational &timecode, const int &divider)
{
// This is a still image
ImageStreamPtr is = std::static_pointer_cast<ImageStream>(stream());
VideoStreamPtr is = std::static_pointer_cast<VideoStream>(stream());
QString img_filename = stream()->footage()->filename();
// If it's an image sequence, we'll probably need to transform the filename
if (stream()->type() == Stream::kVideo) {
if (is->video_type() == VideoStream::kVideoTypeImageSequence) {
int64_t ts = std::static_pointer_cast<VideoStream>(stream())->get_time_in_timebase_units(timecode);
img_filename = TransformImageSequenceFileName(stream()->footage()->filename(), ts);
@@ -173,14 +173,14 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid
return nullptr;
}
if (stream()->type() != Stream::kImage && stream()->type() != Stream::kVideo) {
if (stream()->type() != Stream::kVideo) {
return nullptr;
}
ImageStreamPtr is = std::static_pointer_cast<ImageStream>(stream());
VideoStreamPtr vs = std::static_pointer_cast<VideoStream>(stream());
if (stream()->type() == Stream::kImage
|| std::static_pointer_cast<VideoStream>(stream())->is_image_sequence()) {
if (vs->video_type() == VideoStream::kVideoTypeStill
|| vs->video_type() == VideoStream::kVideoTypeImageSequence) {
return RetrieveStillImage(timecode, divider);
@@ -188,8 +188,6 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid
FFmpegFramePool::ElementPtr return_frame = nullptr;
VideoStreamPtr vs = std::static_pointer_cast<VideoStream>(stream());
int64_t target_ts = vs->get_time_in_timebase_units(timecode);
FFmpegDecoderInstance* working_instance = nullptr;
@@ -450,26 +448,21 @@ bool FFmpegDecoder::SupportsAudio()
return true;
}
bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
ItemPtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
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;
FootagePtr footage = nullptr;
// Convert QString to a C string
QByteArray ba = f->filename().toUtf8();
const char* filename = ba.constData();
QByteArray ba = filename.toUtf8();
const char* filename_c = ba.constData();
// Open file in a format context
AVFormatContext* fmt_ctx = nullptr;
error_code = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
error_code = avformat_open_input(&fmt_ctx, filename_c, nullptr, nullptr);
// Handle format context error
if (error_code == 0) {
@@ -502,7 +495,7 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
AVFrame* frame = av_frame_alloc();
{
FFmpegDecoderInstance instance(filename, i);
FFmpegDecoderInstance instance(filename_c, i);
// Read first frame and retrieve some metadata
if (instance.GetFrame(pkt, frame) >= 0) {
@@ -548,26 +541,24 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
av_packet_free(&pkt);
}
ImageStreamPtr image_stream;
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
if (image_is_still) {
image_stream = std::make_shared<ImageStream>();
video_stream->set_video_type(VideoStream::kVideoTypeStill);
} else {
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
video_stream->set_video_type(VideoStream::kVideoTypeVideo);
video_stream->set_frame_rate(frame_rate);
video_stream->set_start_time(avstream->start_time);
image_stream = video_stream;
}
image_stream->set_width(avstream->codecpar->width);
image_stream->set_height(avstream->codecpar->height);
image_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast<AVPixelFormat>(avstream->codecpar->format))));
image_stream->set_interlacing(interlacing);
image_stream->set_pixel_aspect_ratio(pixel_aspect_ratio);
video_stream->set_width(avstream->codecpar->width);
video_stream->set_height(avstream->codecpar->height);
video_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast<AVPixelFormat>(avstream->codecpar->format))));
video_stream->set_interlacing(interlacing);
video_stream->set_pixel_aspect_ratio(pixel_aspect_ratio);
str = image_stream;
str = video_stream;
} else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && decoder) {
@@ -630,19 +621,20 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
}
if (found_valid_streams) {
// We actually have footage we can return instead of nullptr
footage = std::make_shared<Footage>();
// Copy streams over
foreach (StreamPtr stream, streams) {
f->add_stream(stream);
footage->add_stream(stream);
}
result = true;
}
}
// Free all memory
avformat_close_input(&fmt_ctx);
return result;
return footage;
}
void FFmpegDecoder::FFmpegError(int error_code)
@@ -834,8 +826,8 @@ FramePtr FFmpegDecoder::BuffersToNativeFrame(int divider, int width, int height,
copy->set_video_params(VideoParams(width,
height,
native_pix_fmt_,
std::static_pointer_cast<ImageStream>(stream())->pixel_aspect_ratio(),
std::static_pointer_cast<ImageStream>(stream())->interlacing(),
std::static_pointer_cast<VideoStream>(stream())->pixel_aspect_ratio(),
std::static_pointer_cast<VideoStream>(stream())->interlacing(),
divider));
copy->set_timestamp(ts);
copy->allocate();
+1 -1
View File
@@ -146,7 +146,7 @@ public:
// Destructor
virtual ~FFmpegDecoder() override;
virtual bool Probe(Footage *f, const QAtomicInt *cancelled) override;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
virtual bool Open() override;
virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override;
+11 -82
View File
@@ -45,86 +45,34 @@ QString OIIODecoder::id()
return QStringLiteral("oiio");
}
bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
ItemPtr OIIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
if (!FileTypeIsSupported(f->filename())) {
return false;
if (!FileTypeIsSupported(filename)) {
return nullptr;
}
std::string std_filename = f->filename().toStdString();
std::string std_filename = filename.toStdString();
auto in = OIIO::ImageInput::open(std_filename);
if (!in) {
return false;
return nullptr;
}
if (!strcmp(in->format_name(), "FFmpeg movie")) {
// If this is FFmpeg via OIIO, fall-through to our native FFmpeg decoder
return false;
return nullptr;
}
is_sequence_ = false;
FootagePtr footage = std::make_shared<Footage>();
// Heuristically determine whether this file is part of an image sequence or not
if (GetImageSequenceDigitCount(f->filename()) > 0) {
QSize dim(in->spec().width, in->spec().height);
int64_t ind = GetImageSequenceIndex(f->filename());
// Check if files around exist around it with that follow a sequence
QString previous_img_fn = TransformImageSequenceFileName(f->filename(), ind - 1);
QString next_img_fn = TransformImageSequenceFileName(f->filename(), ind + 1);
// GetImageDimensions will return a 0,0 size if the file doesn't exist, so it's safe to check
// both existence and matching size with this
if (GetImageDimensions(previous_img_fn) == dim || GetImageDimensions(next_img_fn) == dim) {
// We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer
// to the Core which will definitely be in the UI thread and block here until we get an answer from the user
QMetaObject::invokeMethod(Core::instance(),
"ConfirmImageSequence",
Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, is_sequence_),
Q_ARG(QString, f->filename()));
}
}
ImageStreamPtr image_stream;
if (is_sequence_) {
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
image_stream = video_stream;
rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value<rational>();
video_stream->set_timebase(default_timebase);
video_stream->set_frame_rate(default_timebase.flipped());
video_stream->set_image_sequence(true);
int64_t seq_index = GetImageSequenceIndex(f->filename());
int64_t start_index = seq_index;
int64_t end_index = seq_index;
// Heuristic to find the first and last images (users can always override this later in FootagePropertiesDialog)
while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), start_index-1))) {
start_index--;
}
while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), end_index+1))) {
end_index++;
}
video_stream->set_start_time(start_index);
video_stream->set_duration(end_index - start_index + 1);
} else {
image_stream = std::make_shared<ImageStream>();
}
VideoStreamPtr image_stream = std::make_shared<VideoStream>();
image_stream->set_width(in->spec().width);
image_stream->set_height(in->spec().height);
image_stream->set_format(GetFormatFromOIIOBasetype(in->spec()));
image_stream->set_pixel_aspect_ratio(GetPixelAspectRatioFromOIIO(in->spec()));
image_stream->set_video_type(VideoStream::kVideoTypeStill);
// Images will always have just one stream
image_stream->set_index(0);
@@ -135,7 +83,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
image_stream->set_premultiplied_alpha(true);
// Get stats for this image and dump them into the Footage file
f->add_stream(image_stream);
footage->add_stream(image_stream);
// If we're here, we have a successful image open
in->close();
@@ -144,7 +92,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
OIIO::ImageInput::destroy(in);
#endif
return true;
return footage;
}
bool OIIODecoder::Open()
@@ -322,25 +270,6 @@ bool OIIODecoder::FileTypeIsSupported(const QString& fn)
return true;
}
QSize OIIODecoder::GetImageDimensions(const QString &fn)
{
QSize sz;
auto in = OIIO::ImageInput::open(fn.toStdString());
if (in) {
sz.setWidth(in->spec().width);
sz.setHeight(in->spec().height);
in->close();
#if OIIO_VERSION < 10903
OIIO::ImageInput::destroy(in);
#endif
}
return sz;
}
bool OIIODecoder::OpenImageHandler(const QString &fn)
{
image_ = OIIO::ImageInput::open(fn.toStdString());
+1 -5
View File
@@ -37,7 +37,7 @@ public:
virtual QString id() override;
virtual bool Probe(Footage *f, const QAtomicInt* cancelled) override;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
virtual bool Open() override;
virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override;
@@ -62,8 +62,6 @@ private:
static bool FileTypeIsSupported(const QString& fn);
static QSize GetImageDimensions(const QString& fn);
bool OpenImageHandler(const QString& fn);
void CloseImageHandle();
@@ -72,8 +70,6 @@ private:
bool is_rgba_;
bool is_sequence_;
OIIO::ImageBuf* buffer_;
static QStringList supported_formats_;
+16 -2
View File
@@ -20,6 +20,8 @@
#include "otiodecoder.h"
#include <opentimelineio/timeline.h>
OLIVE_NAMESPACE_ENTER
OTIODecoder::OTIODecoder()
@@ -32,9 +34,21 @@ QString OTIODecoder::id()
return QStringLiteral("otio");
}
bool OTIODecoder::Probe(Footage* f, const QAtomicInt* cancelled)
ItemPtr OTIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
return false;
if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) {
opentimelineio::v1_0::ErrorStatus es;
auto timeline = static_cast<opentimelineio::v1_0::Timeline*>(opentimelineio::v1_0::SerializableObjectWithMetadata::from_json_file(filename.toStdString(), &es));
if (es != opentimelineio::v1_0::ErrorStatus::OK) {
return nullptr;
}
qDebug() << "Found" << timeline->video_tracks().size() << "video tracks";
}
return nullptr;
}
OLIVE_NAMESPACE_EXIT
+4 -1
View File
@@ -33,7 +33,10 @@ public:
virtual QString id() override;
virtual bool Probe(Footage* f, const QAtomicInt* cancelled) override;
virtual bool Open() override {return false;}
virtual void Close() override {}
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
};