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:
+18
-20
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 ÷r)
|
||||
{
|
||||
// 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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user