From 6db591e89fa5257459b875a0149556e9fea51405 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 26 Sep 2020 19:29:00 +1000 Subject: [PATCH] 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. --- app/codec/decoder.cpp | 38 ++-- app/codec/decoder.h | 16 +- app/codec/ffmpeg/ffmpegdecoder.cpp | 66 +++--- app/codec/ffmpeg/ffmpegdecoder.h | 2 +- app/codec/oiio/oiiodecoder.cpp | 93 +-------- app/codec/oiio/oiiodecoder.h | 6 +- app/codec/otio/otiodecoder.cpp | 18 +- app/codec/otio/otiodecoder.h | 5 +- .../footageproperties/footageproperties.cpp | 6 +- .../videostreamproperties.cpp | 15 +- .../streamproperties/videostreamproperties.h | 10 +- app/node/node.cpp | 6 +- app/node/traverser.cpp | 3 +- app/panel/footageviewer/footageviewer.cpp | 5 + app/project/item/footage/CMakeLists.txt | 2 - app/project/item/footage/audiostream.cpp | 5 + app/project/item/footage/audiostream.h | 2 + app/project/item/footage/footage.cpp | 122 +++-------- app/project/item/footage/footage.h | 45 ++-- app/project/item/footage/imagestream.cpp | 136 ------------ app/project/item/footage/imagestream.h | 137 ------------ app/project/item/footage/stream.cpp | 18 +- app/project/item/footage/stream.h | 8 +- app/project/item/footage/videostream.cpp | 197 ++++++++---------- app/project/item/footage/videostream.h | 125 +++++++++-- app/project/item/sequence/sequence.cpp | 32 ++- app/project/project.cpp | 31 +++ app/project/project.h | 5 + app/render/backend/opengl/openglproxy.cpp | 2 +- app/render/backend/renderworker.cpp | 4 +- app/task/project/import/import.cpp | 193 +++++++++++++++-- app/task/project/import/import.h | 13 +- .../footagecombobox/footagecombobox.cpp | 6 +- .../nodetableview/nodetabletraverser.cpp | 2 +- app/widget/timelinewidget/tool/import.cpp | 13 +- app/widget/viewer/footageviewer.cpp | 3 +- app/widget/viewer/gizmotraverser.cpp | 2 +- 37 files changed, 613 insertions(+), 779 deletions(-) delete mode 100644 app/project/item/footage/imagestream.cpp delete mode 100644 app/project/item/footage/imagestream.h diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 38b67c102..a1a05cee5 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -98,23 +98,20 @@ QVector 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 decoder_list = ReceiveListOfAllDecoders(); @@ -122,30 +119,31 @@ bool Decoder::ProbeMedia(Footage *f, const QAtomicInt* cancelled) for (int i=0;iProbe(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(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) diff --git a/app/codec/decoder.h b/app/codec/decoder.h index b7cf27472..cca3ce0b6 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -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: diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 46cac10fc..d71b9455c 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -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(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(stream()); + VideoStreamPtr is = std::static_pointer_cast(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(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(stream()); + VideoStreamPtr vs = std::static_pointer_cast(stream()); - if (stream()->type() == Stream::kImage - || std::static_pointer_cast(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(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(); if (image_is_still) { - image_stream = std::make_shared(); + video_stream->set_video_type(VideoStream::kVideoTypeStill); } else { - VideoStreamPtr video_stream = std::make_shared(); + 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(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(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(); + // 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(stream())->pixel_aspect_ratio(), - std::static_pointer_cast(stream())->interlacing(), + std::static_pointer_cast(stream())->pixel_aspect_ratio(), + std::static_pointer_cast(stream())->interlacing(), divider)); copy->set_timestamp(ts); copy->allocate(); diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index e8a5ed296..983b686c0 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -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; diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index bfaf46960..fff9893e5 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -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(); - // 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(); - image_stream = video_stream; - - rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value(); - 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(); - } + VideoStreamPtr image_stream = std::make_shared(); 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()); diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index 46d9228cb..79a23019c 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -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_; diff --git a/app/codec/otio/otiodecoder.cpp b/app/codec/otio/otiodecoder.cpp index b0754a6df..2485b9733 100644 --- a/app/codec/otio/otiodecoder.cpp +++ b/app/codec/otio/otiodecoder.cpp @@ -20,6 +20,8 @@ #include "otiodecoder.h" +#include + 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::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 diff --git a/app/codec/otio/otiodecoder.h b/app/codec/otio/otiodecoder.h index a88d9ad68..dd88567cd 100644 --- a/app/codec/otio/otiodecoder.h +++ b/app/codec/otio/otiodecoder.h @@ -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; }; diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index 484127021..d1dbd0fe7 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -78,8 +78,7 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota switch (stream->type()) { case Stream::kVideo: - case Stream::kImage: - stacked_widget_->addWidget(new VideoStreamProperties(std::static_pointer_cast(stream))); + stacked_widget_->addWidget(new VideoStreamProperties(std::static_pointer_cast(stream))); break; case Stream::kAudio: stacked_widget_->addWidget(new AudioStreamProperties(std::static_pointer_cast(stream))); @@ -90,8 +89,7 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota if (first_usable_stream == -1 && (stream->type() == Stream::kVideo - || stream->type() == Stream::kAudio - || stream->type() == Stream::kImage)) { + || stream->type() == Stream::kAudio)) { first_usable_stream = i; } } diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index f42da0d1a..0aaca3a43 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -35,7 +35,7 @@ namespace OCIO = OCIO_NAMESPACE::v1; OLIVE_NAMESPACE_ENTER -VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : +VideoStreamProperties::VideoStreamProperties(VideoStreamPtr stream) : stream_(stream) { QGridLayout* video_layout = new QGridLayout(this); @@ -86,7 +86,7 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : row++; - if (IsImageSequence(stream.get())) { + if (stream->video_type() == VideoStream::kVideoTypeImageSequence) { QGroupBox* imgseq_group = new QGroupBox(tr("Image Sequence")); QGridLayout* imgseq_layout = new QGridLayout(imgseq_group); @@ -143,7 +143,7 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) parent); } - if (IsImageSequence(stream_.get())) { + if (stream_->video_type() == VideoStream::kVideoTypeImageSequence) { VideoStreamPtr video_stream = std::static_pointer_cast(stream_); int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue() + 1; @@ -162,7 +162,7 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) bool VideoStreamProperties::SanityCheck() { - if (IsImageSequence(stream_.get())) { + if (stream_->video_type() == VideoStream::kVideoTypeImageSequence) { if (imgseq_start_time_->GetValue() >= imgseq_end_time_->GetValue()) { QMessageBox::critical(this, tr("Invalid Configuration"), @@ -175,12 +175,7 @@ bool VideoStreamProperties::SanityCheck() return true; } -bool VideoStreamProperties::IsImageSequence(ImageStream *stream) -{ - return (stream->type() == Stream::kVideo && static_cast(stream)->is_image_sequence()); -} - -VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream, +VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(VideoStreamPtr stream, bool premultiplied, QString colorspace, VideoParams::Interlacing interlacing, diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index e26a2e5bb..2b82b80d1 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -35,19 +35,17 @@ OLIVE_NAMESPACE_ENTER class VideoStreamProperties : public StreamProperties { public: - VideoStreamProperties(ImageStreamPtr stream); + VideoStreamProperties(VideoStreamPtr stream); virtual void Accept(QUndoCommand* parent) override; virtual bool SanityCheck() override; private: - static bool IsImageSequence(ImageStream* stream); - /** * @brief Attached video stream */ - ImageStreamPtr stream_; + VideoStreamPtr stream_; /** * @brief Setting for associated/premultiplied alpha @@ -86,7 +84,7 @@ private: class VideoStreamChangeCommand : public UndoCommand { public: - VideoStreamChangeCommand(ImageStreamPtr stream, + VideoStreamChangeCommand(VideoStreamPtr stream, bool premultiplied, QString colorspace, VideoParams::Interlacing interlacing, @@ -100,7 +98,7 @@ private: virtual void undo_internal() override; private: - ImageStreamPtr stream_; + VideoStreamPtr stream_; bool new_premultiplied_; QString new_colorspace_; diff --git a/app/node/node.cpp b/app/node/node.cpp index 15e28512a..9c5e4a4d5 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -28,7 +28,7 @@ #include "common/xmlutils.h" #include "project/project.h" #include "project/item/footage/footage.h" -#include "project/item/footage/imagestream.h" +#include "project/item/footage/videostream.h" OLIVE_NAMESPACE_ENTER @@ -366,8 +366,8 @@ void Node::Hash(QCryptographicHash &hash, const rational& time) const // Footage stream hash.addData(QString::number(stream->index()).toUtf8()); - if (stream->type() == Stream::kImage || stream->type() == Stream::kVideo) { - ImageStreamPtr image_stream = std::static_pointer_cast(stream); + if (stream->type() == Stream::kVideo) { + VideoStreamPtr image_stream = std::static_pointer_cast(stream); // Current color config and space hash.addData(image_stream->footage()->project()->color_manager()->GetConfigFilename().toUtf8()); diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 5ba40748f..f8958f28e 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -189,8 +189,7 @@ void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, N StreamPtr s = v.data().value(); if (s) { - if (s->type() == Stream::kVideo - || s->type() == Stream::kImage) { + if (s->type() == Stream::kVideo) { take_this_value_list = &video_footage_to_retrieve; } else if (s->type() == Stream::kAudio) { take_this_value_list = &audio_footage_to_retrieve; diff --git a/app/panel/footageviewer/footageviewer.cpp b/app/panel/footageviewer/footageviewer.cpp index 608f7f8cc..25a278ce0 100644 --- a/app/panel/footageviewer/footageviewer.cpp +++ b/app/panel/footageviewer/footageviewer.cpp @@ -51,6 +51,11 @@ QList FootageViewerPanel::GetSelectedFootage() const void FootageViewerPanel::SetFootage(Footage *f) { + if (!f->IsValid()) { + // Do nothing if footage is invalid + return; + } + static_cast(GetTimeBasedWidget())->SetFootage(f); if (f) { diff --git a/app/project/item/footage/CMakeLists.txt b/app/project/item/footage/CMakeLists.txt index b533f5530..5482cce07 100644 --- a/app/project/item/footage/CMakeLists.txt +++ b/app/project/item/footage/CMakeLists.txt @@ -21,8 +21,6 @@ set(OLIVE_SOURCES project/item/footage/audiostream.cpp project/item/footage/footage.h project/item/footage/footage.cpp - project/item/footage/imagestream.h - project/item/footage/imagestream.cpp project/item/footage/stream.h project/item/footage/stream.cpp project/item/footage/videostream.h diff --git a/app/project/item/footage/audiostream.cpp b/app/project/item/footage/audiostream.cpp index 4e81fe521..7aec20aa5 100644 --- a/app/project/item/footage/audiostream.cpp +++ b/app/project/item/footage/audiostream.cpp @@ -96,4 +96,9 @@ void AudioStream::append_conformed_version(const AudioParams ¶ms) emit ConformAppended(params); } +QIcon AudioStream::icon() const +{ + return icon::Audio; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/project/item/footage/audiostream.h b/app/project/item/footage/audiostream.h index 50dcfe3d1..5bf90072f 100644 --- a/app/project/item/footage/audiostream.h +++ b/app/project/item/footage/audiostream.h @@ -53,6 +53,8 @@ public: bool has_conformed_version(const AudioParams& params); void append_conformed_version(const AudioParams& params); + virtual QIcon icon() const override; + signals: void ConformAppended(OLIVE_NAMESPACE::AudioParams params); diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 56136de0f..e9e83d706 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -43,6 +43,7 @@ Footage::~Footage() void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) { + /* QXmlStreamAttributes attributes = reader->attributes(); foreach (const QXmlStreamAttribute& attr, attributes) { @@ -110,6 +111,7 @@ void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const Q reader->skipCurrentElement(); } } + */ } void Footage::Save(QXmlStreamWriter *writer) const @@ -128,25 +130,18 @@ void Footage::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // footage } -const Footage::Status& Footage::status() const -{ - return status_; -} - -void Footage::set_status(const Footage::Status &status) -{ - status_ = status; - - UpdateTooltip(); -} - void Footage::Clear() { // Clear all streams ClearStreams(); // Reset ready state - set_status(kUnprobed); + valid_ = false; +} + +void Footage::SetValid() +{ + valid_ = true; } const QString &Footage::filename() const @@ -210,34 +205,21 @@ void Footage::set_decoder(const QString &id) QIcon Footage::icon() { - switch (status_) { - case kUnprobed: - case kUnindexed: - // FIXME Set a waiting icon - return QIcon(); - case kReady: - if (HasStreamsOfType(Stream::kVideo)) { + if (valid_ && !streams_.isEmpty()) { + StreamPtr first_stream = streams_.first(); - // Prioritize the video icon - return icon::Video; - - } else if (HasStreamsOfType(Stream::kAudio)) { - - // Otherwise assume it's audio only + if (first_stream->type() == Stream::kVideo) { + if (std::static_pointer_cast(first_stream)->video_type() == VideoStream::kVideoTypeStill) { + return icon::Image; + } else { + return icon::Video; + } + } else if (first_stream->type() == Stream::kAudio) { return icon::Audio; - - } else if (HasStreamsOfType(Stream::kImage)) { - - // Otherwise assume it's an image - return icon::Image; - } - /* fall-through */ - case kInvalid: - return icon::Error; } - return QIcon(); + return icon::Error; } QString Footage::duration() @@ -248,7 +230,7 @@ QString Footage::duration() rational longest; foreach (StreamPtr stream, streams_) { - if (stream->type() == Stream::kVideo || stream->type() == Stream::kAudio) { + if (stream->enabled() && (stream->type() == Stream::kVideo || stream->type() == Stream::kAudio)) { rational this_stream_dur = Timecode::timestamp_to_time(stream->duration(), stream->timebase()); @@ -300,7 +282,8 @@ QString Footage::rate() return QString(); } - if (HasStreamsOfType(Stream::kVideo)) { + if (HasStreamsOfType(Stream::kVideo) + && std::static_pointer_cast(get_first_stream_of_type(Stream::kVideo))->video_type() != VideoStream::kVideoTypeStill) { // This is a video editor, prioritize video streams VideoStreamPtr video_stream = std::static_pointer_cast(get_first_stream_of_type(Stream::kVideo)); return QCoreApplication::translate("Footage", "%1 FPS").arg(video_stream->frame_rate().toDouble()); @@ -331,10 +314,6 @@ quint64 Footage::get_enabled_stream_flags() const void Footage::ClearStreams() { - if (streams_.empty()) { - return; - } - // Delete all streams streams_.clear(); } @@ -343,7 +322,7 @@ bool Footage::HasStreamsOfType(const Stream::Type &type) const { // Return true if any streams are video streams foreach (StreamPtr stream, streams_) { - if (stream->type() == type) { + if (stream->enabled() && stream->type() == type) { return true; } } @@ -354,7 +333,7 @@ bool Footage::HasStreamsOfType(const Stream::Type &type) const StreamPtr Footage::get_first_stream_of_type(const Stream::Type &type) const { foreach (StreamPtr stream, streams_) { - if (stream->type() == type) { + if (stream->enabled() && stream->type() == type) { return stream; } } @@ -364,62 +343,21 @@ StreamPtr Footage::get_first_stream_of_type(const Stream::Type &type) const void Footage::UpdateTooltip() { - switch (status_) { - case kUnprobed: - set_tooltip(QCoreApplication::translate("Footage", "Waiting for probe")); - break; - case kUnindexed: - set_tooltip(QCoreApplication::translate("Footage", "Waiting for index")); - break; - case kReady: - { + if (valid_) { QString tip = QCoreApplication::translate("Footage", "Filename: %1").arg(filename()); if (!streams_.isEmpty()) { - tip.append("\n"); - - for (int i=0;itype()) { - case Stream::kVideo: - case Stream::kImage: - { - ImageStreamPtr vs = std::static_pointer_cast(s); - - tip.append( - QCoreApplication::translate("Footage", - "\nVideo %1: %2x%3").arg(QString::number(i), - QString::number(vs->width()), - QString::number(vs->height())) - ); - break; - } - case Stream::kAudio: - { - AudioStreamPtr as = std::static_pointer_cast(s); - - tip.append( - QCoreApplication::translate("Footage", - "\nAudio %1: %2 channels %3 Hz").arg(QString::number(i), - QString::number(as->channels()), - QString::number(as->sample_rate())) - ); - break; - } - default: - break; + foreach (StreamPtr s, streams_) { + if (s->enabled()) { + tip.append("\n"); + tip.append(s->description()); } } } set_tooltip(tip); - } - break; - case kInvalid: - set_tooltip(QCoreApplication::translate("Footage", "An error occurred probing this footage")); - break; + } else { + set_tooltip(QCoreApplication::translate("Footage", "This footage is not valid for use")); } } diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index 2bcf485a5..6f0b9735e 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -27,7 +27,6 @@ #include "common/rational.h" #include "project/item/item.h" #include "project/item/footage/audiostream.h" -#include "project/item/footage/imagestream.h" #include "project/item/footage/videostream.h" #include "timeline/timelinepoints.h" @@ -43,13 +42,6 @@ OLIVE_NAMESPACE_ENTER class Footage : public Item, public TimelinePoints { public: - enum Status { - kUnprobed, - kUnindexed, - kReady, - kInvalid - }; - /** * @brief Footage Constructor */ @@ -72,26 +64,6 @@ public: */ virtual void Save(QXmlStreamWriter *writer) const override; - /** - * @brief Check the ready state of this Footage object - * - * @return - * - * If the Footage has been successfully probed, this will return TRUE. - */ - const Status& status() const; - - /** - * @brief Set ready state - * - * This should only be set by olive::ProbeMedia. Sets the Footage's current status to a member of enum - * Footage::Status. - * - * This function also runs UpdateIcon() and UpdateTooltip(). If you need to override the tooltip (e.g. for an error - * message), you must run set_tooltip() *after* running set_status(); - */ - void set_status(const Status& status); - /** * @brief Reset Footage state ready for running through Probe() again * @@ -104,6 +76,16 @@ public: */ void Clear(); + bool IsValid() const + { + return valid_; + } + + /** + * @brief Sets this footage to valid and ready to use + */ + void SetValid(); + /** * @brief Return the current filename of this Footage object */ @@ -254,16 +236,13 @@ private: */ QList streams_; - /** - * @brief Internal ready setting - */ - Status status_; - /** * @brief Internal attached decoder ID */ QString decoder_; + bool valid_; + }; using FootagePtr = std::shared_ptr; diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp deleted file mode 100644 index a4a53df0a..000000000 --- a/app/project/item/footage/imagestream.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/*** - - 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 . - -***/ - -#include "imagestream.h" - -#include "common/xmlutils.h" -#include "footage.h" -#include "project/project.h" -#include "render/colormanager.h" - -OLIVE_NAMESPACE_ENTER - -ImageStream::ImageStream() : - premultiplied_alpha_(false), - interlacing_(VideoParams::kInterlaceNone), - pixel_aspect_ratio_(1) -{ - set_type(kImage); -} - -void ImageStream::FootageSetEvent(Footage *f) -{ - // For some reason this connection fails if we don't explicitly specify DirectConnection - connect(f->project()->color_manager(), - &ColorManager::ConfigChanged, - this, - &ImageStream::ColorConfigChanged, - Qt::DirectConnection); - - connect(f->project()->color_manager(), - &ColorManager::DefaultInputColorSpaceChanged, - this, - &ImageStream::DefaultColorSpaceChanged, - Qt::DirectConnection); -} - -void ImageStream::LoadCustomParameters(QXmlStreamReader *reader) -{ - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("colorspace")) { - set_colorspace(reader->readElementText()); - } else { - reader->skipCurrentElement(); - } - } -} - -void ImageStream::SaveCustomParameters(QXmlStreamWriter *writer) const -{ - writer->writeTextElement("colorspace", colorspace_); -} - -QString ImageStream::description() const -{ - return QCoreApplication::translate("Stream", "%1: Image - %2x%3").arg(QString::number(index()), - QString::number(width()), - QString::number(height())); -} - -bool ImageStream::premultiplied_alpha() const -{ - return premultiplied_alpha_; -} - -void ImageStream::set_premultiplied_alpha(bool e) -{ - premultiplied_alpha_ = e; - - emit ParametersChanged(); -} - -const QString &ImageStream::colorspace(bool default_if_empty) const -{ - if (colorspace_.isEmpty() && default_if_empty) { - return footage()->project()->color_manager()->GetDefaultInputColorSpace(); - } else { - return colorspace_; - } -} - -void ImageStream::set_colorspace(const QString &color) -{ - colorspace_ = color; - - emit ParametersChanged(); -} - -QString ImageStream::get_colorspace_match_string() const -{ - return QStringLiteral("%1:%2").arg(footage()->project()->color_manager()->GetConfigFilename(), - colorspace()); -} - -void ImageStream::ColorConfigChanged() -{ - ColorManager* color_manager = footage()->project()->color_manager(); - - // Check if this colorspace is in the new config - if (!colorspace_.isEmpty()) { - QStringList colorspaces = color_manager->ListAvailableColorspaces(); - if (!colorspaces.contains(colorspace_)) { - // Set to empty if not - colorspace_.clear(); - } - } - - // Either way, the color calculation has likely changed so we signal here - emit ParametersChanged(); -} - -void ImageStream::DefaultColorSpaceChanged() -{ - // If no colorspace is set, this stream uses the default color space and it's just changed - if (colorspace_.isEmpty()) { - emit ParametersChanged(); - } -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h deleted file mode 100644 index dfa01bab9..000000000 --- a/app/project/item/footage/imagestream.h +++ /dev/null @@ -1,137 +0,0 @@ -/*** - - 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 . - -***/ - -#ifndef IMAGESTREAM_H -#define IMAGESTREAM_H - -#include "render/pixelformat.h" -#include "render/videoparams.h" -#include "stream.h" - -OLIVE_NAMESPACE_ENTER - -/** - * @brief A Stream derivative containing video-specific information - */ -class ImageStream : public Stream -{ - Q_OBJECT -public: - ImageStream(); - - virtual QString description() const override; - - const int& width() const - { - return width_; - } - - void set_width(const int& width) - { - width_ = width; - } - - const int& height() const - { - return height_; - } - - void set_height(const int& height) - { - height_ = height; - } - - const PixelFormat::Format& format() const - { - return format_; - } - - void set_format(const PixelFormat::Format& format) - { - format_ = format; - } - - bool premultiplied_alpha() const; - void set_premultiplied_alpha(bool e); - - const QString& colorspace(bool default_if_empty = true) const; - void set_colorspace(const QString& color); - - QString get_colorspace_match_string() const; - - VideoParams::Interlacing interlacing() const - { - return interlacing_; - } - - void set_interlacing(VideoParams::Interlacing i) - { - interlacing_ = i; - - emit ParametersChanged(); - } - - const rational& pixel_aspect_ratio() const - { - return pixel_aspect_ratio_; - } - - void set_pixel_aspect_ratio(const rational& r) - { - // Auto-correct null aspect ratio to 1:1 - if (r.isNull()) { - pixel_aspect_ratio_ = 1; - } else { - pixel_aspect_ratio_ = r; - } - - emit ParametersChanged(); - } - -protected: - virtual void FootageSetEvent(Footage*) override; - - virtual void LoadCustomParameters(QXmlStreamReader *reader) override; - - virtual void SaveCustomParameters(QXmlStreamWriter* writer) const override; - -private: - int width_; - int height_; - bool premultiplied_alpha_; - QString colorspace_; - VideoParams::Interlacing interlacing_; - - PixelFormat::Format format_; - - rational pixel_aspect_ratio_; - -private slots: - void ColorConfigChanged(); - - void DefaultColorSpaceChanged(); - -}; - -using ImageStreamPtr = std::shared_ptr; - -OLIVE_NAMESPACE_EXIT - -#endif // IMAGESTREAM_H diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index 71f0b7993..2a59070ea 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -78,7 +78,6 @@ Footage *Stream::footage() const void Stream::set_footage(Footage *f) { footage_ = f; - FootageSetEvent(footage_); } const rational &Stream::timebase() const @@ -123,19 +122,8 @@ void Stream::set_enabled(bool e) enabled_ = e; } -QIcon Stream::IconFromType(const Stream::Type &type) +QIcon Stream::icon() const { - switch (type) { - case Stream::kVideo: - return icon::Video; - case Stream::kImage: - return icon::Image; - case Stream::kAudio: - return icon::Audio; - default: - break; - } - return QIcon(); } @@ -144,10 +132,6 @@ QMutex *Stream::proxy_access_lock() return &proxy_access_lock_; } -void Stream::FootageSetEvent(Footage*) -{ -} - void Stream::LoadCustomParameters(QXmlStreamReader* reader) { reader->skipCurrentElement(); diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index 240f01b36..e5165de53 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -28,6 +28,7 @@ #include #include "common/rational.h" +#include "ui/icons/icons.h" OLIVE_NAMESPACE_ENTER @@ -52,8 +53,7 @@ public: kAudio, kData, kSubtitle, - kAttachment, - kImage = 100 + kAttachment }; /** @@ -90,13 +90,11 @@ public: bool enabled() const; void set_enabled(bool e); - static QIcon IconFromType(const Type& type); + virtual QIcon icon() const; QMutex* proxy_access_lock(); protected: - virtual void FootageSetEvent(Footage*); - virtual void LoadCustomParameters(QXmlStreamReader *reader); virtual void SaveCustomParameters(QXmlStreamWriter* writer) const; diff --git a/app/project/item/footage/videostream.cpp b/app/project/item/footage/videostream.cpp index 62dbaff4c..65c658eff 100644 --- a/app/project/item/footage/videostream.cpp +++ b/app/project/item/footage/videostream.cpp @@ -23,21 +23,35 @@ #include #include "common/timecodefunctions.h" +#include "common/xmlutils.h" +#include "footage.h" +#include "project/project.h" +#include "render/colormanager.h" OLIVE_NAMESPACE_ENTER VideoStream::VideoStream() : + premultiplied_alpha_(false), + interlacing_(VideoParams::kInterlaceNone), + video_type_(VideoStream::kVideoTypeVideo), + pixel_aspect_ratio_(1), start_time_(0), is_image_sequence_(false) { - set_type(kVideo); + set_type(Stream::kVideo); } QString VideoStream::description() const { - return QCoreApplication::translate("Stream", "%1: Video - %2x%3").arg(QString::number(index()), - QString::number(width()), - QString::number(height())); + if (video_type_ == VideoStream::kVideoTypeStill) { + return QCoreApplication::translate("Stream", "%1: Image - %2x%3").arg(QString::number(index()), + QString::number(width()), + QString::number(height())); + } else { + return QCoreApplication::translate("Stream", "%1: Video - %2x%3").arg(QString::number(index()), + QString::number(width()), + QString::number(height())); + } } const rational &VideoStream::frame_rate() const @@ -76,125 +90,88 @@ int64_t VideoStream::get_time_in_timebase_units(const rational &time) const return Timecode::time_to_timestamp(time, timebase()) + start_time(); } -/* -int64_t VideoStream::get_closest_timestamp_in_frame_index(const rational &time) +QIcon VideoStream::icon() const { - // Get rough approximation of what the timestamp would be in this timebase - int64_t target_ts = Timecode::time_to_timestamp(time, timebase()); - - // Find closest actual timebase in the file - return get_closest_timestamp_in_frame_index(target_ts); + if (video_type_ == kVideoTypeStill) { + return icon::Image; + } else { + return icon::Video; + } } -int64_t VideoStream::get_closest_timestamp_in_frame_index(int64_t timestamp) +void VideoStream::LoadCustomParameters(QXmlStreamReader *reader) { - QMutexLocker locker(proxy_access_lock()); - - if (!frame_index_.isEmpty()) { - if (timestamp <= frame_index_.first()) { - return frame_index_.first(); - } else if (timestamp >= frame_index_.last()) { - return frame_index_.last(); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("colorspace")) { + set_colorspace(reader->readElementText()); } else { - // Use index to find closest frame in file - for (int i=1;iskipCurrentElement(); + } + } +} - if (this_ts == timestamp) { - return timestamp; - } else if (this_ts > timestamp) { - return frame_index_.at(i - 1); - } - } +void VideoStream::SaveCustomParameters(QXmlStreamWriter *writer) const +{ + writer->writeTextElement("colorspace", colorspace_); +} + +bool VideoStream::premultiplied_alpha() const +{ + return premultiplied_alpha_; +} + +void VideoStream::set_premultiplied_alpha(bool e) +{ + premultiplied_alpha_ = e; + + emit ParametersChanged(); +} + +const QString &VideoStream::colorspace(bool default_if_empty) const +{ + if (colorspace_.isEmpty() && default_if_empty) { + return footage()->project()->color_manager()->GetDefaultInputColorSpace(); + } else { + return colorspace_; + } +} + +void VideoStream::set_colorspace(const QString &color) +{ + colorspace_ = color; + + emit ParametersChanged(); +} + +QString VideoStream::get_colorspace_match_string() const +{ + return QStringLiteral("%1:%2").arg(footage()->project()->color_manager()->GetConfigFilename(), + colorspace()); +} + +void VideoStream::ColorConfigChanged() +{ + ColorManager* color_manager = footage()->project()->color_manager(); + + // Check if this colorspace is in the new config + if (!colorspace_.isEmpty()) { + QStringList colorspaces = color_manager->ListAvailableColorspaces(); + if (!colorspaces.contains(colorspace_)) { + // Set to empty if not + colorspace_.clear(); } } - return -1; + // Either way, the color calculation has likely changed so we signal here + emit ParametersChanged(); } -*/ -/* -void VideoStream::clear_frame_index() +void VideoStream::DefaultColorSpaceChanged() { - { - QMutexLocker locker(&index_access_lock_); - - frame_index_.clear(); + // If no colorspace is set, this stream uses the default color space and it's just changed + if (colorspace_.isEmpty()) { + emit ParametersChanged(); } - - emit IndexChanged(); } -void VideoStream::append_frame_index(const int64_t &ts) -{ - { - QMutexLocker locker(&index_access_lock_); - - frame_index_.append(ts); - } - - emit IndexChanged(); -} - -bool VideoStream::is_frame_index_ready() -{ - QMutexLocker locker(&index_access_lock_); - - return !frame_index_.isEmpty() && frame_index_.last() == VideoStream::kEndTimestamp; -} - -int64_t VideoStream::last_frame_index_timestamp() -{ - QMutexLocker locker(&index_access_lock_); - - return frame_index_.last(); -} - -bool VideoStream::load_frame_index(const QString &s) -{ - // Load index from file - QFile index_file(s); - - if (index_file.exists() && index_file.open(QFile::ReadOnly)) { - { - QMutexLocker locker(&index_access_lock_); - - // Resize based on filesize - frame_index_.resize(static_cast(index_file.size()) / sizeof(int64_t)); - - // Read frame index into vector - index_file.read(reinterpret_cast(frame_index_.data()), - index_file.size()); - } - - index_file.close(); - - emit IndexChanged(); - - return true; - } - - return false; -} - -bool VideoStream::save_frame_index(const QString &s) -{ - QFile index_file(s); - - if (index_file.open(QFile::WriteOnly)) { - // Write index in binary - QMutexLocker locker(&index_access_lock_); - - index_file.write(reinterpret_cast(frame_index_.constData()), - frame_index_.size() * static_cast(sizeof(int64_t))); - - index_file.close(); - - return true; - } - - return false; -} -*/ - OLIVE_NAMESPACE_EXIT diff --git a/app/project/item/footage/videostream.h b/app/project/item/footage/videostream.h index 6140f7c15..b9f878d0a 100644 --- a/app/project/item/footage/videostream.h +++ b/app/project/item/footage/videostream.h @@ -21,18 +21,106 @@ #ifndef VIDEOSTREAM_H #define VIDEOSTREAM_H -#include "imagestream.h" +#include "render/pixelformat.h" +#include "render/videoparams.h" +#include "stream.h" OLIVE_NAMESPACE_ENTER -class VideoStream : public ImageStream +/** + * @brief A Stream derivative containing video-specific information + */ +class VideoStream : public Stream { Q_OBJECT public: VideoStream(); + enum VideoType { + kVideoTypeVideo, + kVideoTypeStill, + kVideoTypeImageSequence + }; + virtual QString description() const override; + VideoType video_type() const + { + return video_type_; + } + + void set_video_type(VideoType t) + { + video_type_ = t; + } + + const int& width() const + { + return width_; + } + + void set_width(const int& width) + { + width_ = width; + } + + const int& height() const + { + return height_; + } + + void set_height(const int& height) + { + height_ = height; + } + + const PixelFormat::Format& format() const + { + return format_; + } + + void set_format(const PixelFormat::Format& format) + { + format_ = format; + } + + bool premultiplied_alpha() const; + void set_premultiplied_alpha(bool e); + + const QString& colorspace(bool default_if_empty = true) const; + void set_colorspace(const QString& color); + + QString get_colorspace_match_string() const; + + VideoParams::Interlacing interlacing() const + { + return interlacing_; + } + + void set_interlacing(VideoParams::Interlacing i) + { + interlacing_ = i; + + emit ParametersChanged(); + } + + const rational& pixel_aspect_ratio() const + { + return pixel_aspect_ratio_; + } + + void set_pixel_aspect_ratio(const rational& r) + { + // Auto-correct null aspect ratio to 1:1 + if (r.isNull()) { + pixel_aspect_ratio_ = 1; + } else { + pixel_aspect_ratio_ = r; + } + + emit ParametersChanged(); + } + /** * @brief Get this video stream's frame rate * @@ -49,23 +137,32 @@ public: int64_t get_time_in_timebase_units(const rational& time) const; - /* - int64_t get_closest_timestamp_in_frame_index(const rational& time); - int64_t get_closest_timestamp_in_frame_index(int64_t timestamp); + virtual QIcon icon() const override; - void clear_frame_index(); - void append_frame_index(const int64_t& ts); - bool is_frame_index_ready(); - int64_t last_frame_index_timestamp(); +public slots: + void ColorConfigChanged(); - bool load_frame_index(const QString& s); - bool save_frame_index(const QString& s); - */ + void DefaultColorSpaceChanged(); + +protected: + virtual void LoadCustomParameters(QXmlStreamReader *reader) override; + + virtual void SaveCustomParameters(QXmlStreamWriter* writer) const override; private: - rational frame_rate_; + int width_; + int height_; + bool premultiplied_alpha_; + QString colorspace_; + VideoParams::Interlacing interlacing_; - //QVector frame_index_; + VideoType video_type_; + + PixelFormat::Format format_; + + rational pixel_aspect_ratio_; + + rational frame_rate_; int64_t start_time_; diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index ede8f6f88..b90feb863 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -278,33 +278,29 @@ void Sequence::set_parameters_from_footage(const QList footage) VideoStream* vs = static_cast(s.get()); // If this is a video stream, use these parameters - if (!found_video_params && !vs->frame_rate().isNull()) { + if (!found_video_params) { + rational using_timebase; + + if (vs->video_type() == VideoStream::kVideoTypeStill) { + // If this is a still image, we'll use it's resolution but won't set + // `found_video_params` in case something with a frame rate comes along which we'll + // prioritize + using_timebase = video_params().time_base(); + } else { + using_timebase = vs->frame_rate().flipped(); + found_video_params = true; + } + set_video_params(VideoParams(vs->width(), vs->height(), - vs->frame_rate().flipped(), + using_timebase, static_cast(Config::Current()["DefaultSequencePreviewFormat"].toInt()), vs->pixel_aspect_ratio(), vs->interlacing(), VideoParams::generate_auto_divider(vs->width(), vs->height()))); - found_video_params = true; } break; } - case Stream::kImage: - if (!found_video_params) { - // If this is an image stream, we'll use it's resolution but won't set `found_video_params` in case - // something with a frame rate comes along which we'll prioritize - ImageStream* is = static_cast(s.get()); - - set_video_params(VideoParams(is->width(), - is->height(), - video_params().time_base(), - static_cast(Config::Current()["DefaultSequencePreviewFormat"].toInt()), - is->pixel_aspect_ratio(), - is->interlacing(), - VideoParams::generate_auto_divider(is->width(), is->height()))); - } - break; case Stream::kAudio: if (!found_audio_params) { AudioStream* as = static_cast(s.get()); diff --git a/app/project/project.cpp b/app/project/project.cpp index fe1e307aa..0208ffa37 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -36,6 +36,11 @@ Project::Project() : autorecovery_saved_(true) { root_.set_project(this); + + connect(&color_manager_, &ColorManager::ConfigChanged, + this, &Project::ColorConfigChanged); + connect(&color_manager_, &ColorManager::DefaultInputColorSpaceChanged, + this, &Project::DefaultColorSpaceChanged); } void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const QAtomicInt* cancelled) @@ -210,4 +215,30 @@ const QString &Project::cache_path(bool default_if_empty) const return cache_path_; } +void Project::ColorConfigChanged() +{ + QList footage = this->get_items_of_type(Item::kFootage); + + foreach (ItemPtr item, footage) { + foreach (StreamPtr s, std::static_pointer_cast(item)->streams()) { + if (s->type() == Stream::kVideo) { + std::static_pointer_cast(s)->ColorConfigChanged(); + } + } + } +} + +void Project::DefaultColorSpaceChanged() +{ + QList footage = this->get_items_of_type(Item::kFootage); + + foreach (ItemPtr item, footage) { + foreach (StreamPtr s, std::static_pointer_cast(item)->streams()) { + if (s->type() == Stream::kVideo) { + std::static_pointer_cast(s)->DefaultColorSpaceChanged(); + } + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/project/project.h b/app/project/project.h index 9fa99ce48..b96dcff85 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -96,6 +96,11 @@ private: QString cache_path_; +private slots: + void ColorConfigChanged(); + + void DefaultColorSpaceChanged(); + }; using ProjectPtr = std::shared_ptr; diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index 7cc61df47..1db21e0b3 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -95,7 +95,7 @@ bool OpenGLProxy::Init() QVariant OpenGLProxy::FrameToValue(FramePtr frame, StreamPtr stream, const VideoParams& params, const RenderMode::Mode& mode) { - ImageStreamPtr video_stream = std::static_pointer_cast(stream); + VideoStreamPtr video_stream = std::static_pointer_cast(stream); // Set up OCIO context QString colorspace_match = video_stream->get_colorspace_match_string(); diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 28e8a50f5..3c0be8ca8 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -369,8 +369,8 @@ DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream) QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &input_time) { - ImageStreamPtr video_stream = std::static_pointer_cast(stream); - rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time; + VideoStreamPtr video_stream = std::static_pointer_cast(stream); + rational time_match = (video_stream->video_type() == VideoStream::kVideoTypeStill) ? 0 : input_time; QString colorspace_match = video_stream->get_colorspace_match_string(); QVariant value; diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index c8cd0a6c9..6a3cd9647 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -23,8 +23,8 @@ #include #include +#include "config/config.h" #include "core.h" -#include "codec/decoder.h" #include "project/item/footage/footage.h" OLIVE_NAMESPACE_ENTER @@ -65,13 +65,15 @@ bool ProjectImportTask::Run() } } -void ProjectImportTask::Import(Folder *folder, const QFileInfoList &import, int &counter, QUndoCommand* parent_command) +void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counter, QUndoCommand* parent_command) { - foreach (const QFileInfo& file_info, import) { + for (int i=0; i(); + QString file_path = file_info.absoluteFilePath(); - f->set_filename(file_info.absoluteFilePath()); - f->set_name(file_info.fileName()); - f->set_timestamp(file_info.lastModified()); + // FIXME: Probe will fail if a project isn't set because ImageStream and its derivatives + // try to connect to the project's ColorManager instance + ItemPtr item = Decoder::ProbeMedia(file_path, &IsCancelled()); - // Probe will fail if a project isn't set because ImageStream and its derivatives try to connect to the project's - // ColorManager instance - // FIXME: Perhaps re-think this approach at some point - f->set_project(model_->project()); + if (item) { + // Setup metadata + item->set_name(file_info.fileName()); + item->set_project(model_->project()); - Decoder::ProbeMedia(f.get(), &IsCancelled()); + if (item->type() == Item::kFootage) { + FootagePtr footage = std::static_pointer_cast(item); - f->set_project(nullptr); + footage->set_filename(file_path); + footage->set_timestamp(file_info.lastModified()); + + // See if this footage is an image sequence + ValidateImageSequence(footage, import, i); + } - if (f->status() == Footage::kInvalid) { - // Add to list so we can tell the user about it later - invalid_files_.append(file_info.absoluteFilePath()); - } else { // Create undoable command that adds the items to the model new ProjectViewModel::AddItemCommand(model_, folder, - f, + item, parent_command); + } else { + // Add to list so we can tell the user about it later + invalid_files_.append(file_info.absoluteFilePath()); } counter++; @@ -141,4 +148,154 @@ void ProjectImportTask::Import(Folder *folder, const QFileInfoList &import, int } } +void ProjectImportTask::ValidateImageSequence(ItemPtr item, QFileInfoList& info_list, int index) +{ + // Heuristically determine whether this file is part of an image sequence or not + if (!ItemIsStillImageFootageOnly(item)) { + return; + } + + FootagePtr footage = std::static_pointer_cast(item); + VideoStreamPtr video_stream = std::static_pointer_cast(footage->streams().first()); + + // By this point we've established that video contains a single still image stream. Now we'll + // see if it ends with numbers. + if (Decoder::GetImageSequenceDigitCount(footage->filename()) > 0 + && !image_sequence_ignore_files_.contains(footage->filename())) { + QSize dim(video_stream->width(), video_stream->height()); + + int64_t ind = Decoder::GetImageSequenceIndex(footage->filename()); + + // Check if files around exist around it with that follow a sequence + QString previous_img_fn = Decoder::TransformImageSequenceFileName(footage->filename(), ind - 1); + QString next_img_fn = Decoder::TransformImageSequenceFileName(footage->filename(), ind + 1); + + // See if the same decoder can retrieve surrounding files + DecoderPtr decoder = Decoder::CreateFromID(footage->decoder()); + ItemPtr previous_file = decoder->Probe(previous_img_fn, nullptr); + ItemPtr next_file = decoder->Probe(next_img_fn, nullptr); + + // Finally see if these files have the same dimensions + if ((previous_file && CompareStillImageSize(previous_file, dim)) + || (next_file && CompareStillImageSize(next_file, dim))) { + // By this point, we've established this file is a still image with a number at the end of + // the filename surrounded by adjacent numbers. It could be a still image! But let's ask the + // user just in case... + bool is_sequence; + + QMetaObject::invokeMethod(Core::instance(), + "ConfirmImageSequence", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, is_sequence), + Q_ARG(QString, footage->filename())); + + int64_t seq_index = Decoder::GetImageSequenceIndex(footage->filename()); + + // Heuristic to find the first and last images (users can always override this later in + // FootagePropertiesDialog) + int64_t start_index = GetImageSequenceLimit(footage->filename(), seq_index, false); + int64_t end_index = GetImageSequenceLimit(footage->filename(), seq_index, true); + + // Depending on the user's choice, either remove them from the list or don't ask for the + // remainders + for (int64_t j=start_index; j<=end_index; j++) { + QString entry_fn = Decoder::TransformImageSequenceFileName(footage->filename(), j); + + if (is_sequence) { + // If this is part of the sequence we're importing here, remove it + for (int i=index+1; iset_video_type(VideoStream::kVideoTypeVideo); + + rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value(); + video_stream->set_timebase(default_timebase); + video_stream->set_frame_rate(default_timebase.flipped()); + video_stream->set_image_sequence(true); + + video_stream->set_start_time(start_index); + video_stream->set_duration(end_index - start_index + 1); + } + } + } +} + +bool ProjectImportTask::ItemIsStillImageFootageOnly(ItemPtr item) +{ + if (item->type() != Item::kFootage) { + // Item isn't footage, definitely isn't an image sequence + return false; + } + + FootagePtr footage = std::static_pointer_cast(item); + + if (footage->stream_count() != 1) { + // Footage with more than one stream (usually video+audio) most likely isn't an image sequence + return false; + } + + if (footage->streams().first()->type() != Stream::kVideo) { + // Footage with no video stream definitely isn't an image sequence + return false; + } + + VideoStreamPtr video_stream = std::static_pointer_cast(footage->streams().first()); + + if (video_stream->video_type() != VideoStream::kVideoTypeStill) { + // If video type is not a still, this definitely isn't a video stream + return false; + } + + return true; +} + +bool ProjectImportTask::CompareStillImageSize(ItemPtr item, const QSize &sz) +{ + if (!ItemIsStillImageFootageOnly(item)) { + return false; + } + + FootagePtr footage = std::static_pointer_cast(item); + VideoStreamPtr video_stream = std::static_pointer_cast(footage->streams().first()); + + return video_stream->width() == sz.width() && video_stream->height() == sz.height(); +} + +int64_t ProjectImportTask::GetImageSequenceLimit(const QString& start_fn, int64_t start, bool up) +{ + QString test_filename; + int test_index; + + forever { + if (up) { + test_index = start + 1; + } else { + test_index = start - 1; + } + + test_filename = Decoder::TransformImageSequenceFileName(start_fn, test_index); + + if (!QFileInfo::exists(test_filename)) { + // Reached end of index + break; + } + + start = test_index; + } + + return test_index; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/project/import/import.h b/app/task/project/import/import.h index 2257fc8ea..de65a3b5d 100644 --- a/app/task/project/import/import.h +++ b/app/task/project/import/import.h @@ -24,6 +24,7 @@ #include #include +#include "codec/decoder.h" #include "project/projectviewmodel.h" #include "task/task.h" @@ -56,7 +57,15 @@ protected: virtual bool Run() override; private: - void Import(Folder* folder, const QFileInfoList &import, int& counter, QUndoCommand *parent_command); + void Import(Folder* folder, QFileInfoList import, int& counter, QUndoCommand *parent_command); + + void ValidateImageSequence(ItemPtr item, QFileInfoList &info_list, int index); + + static bool ItemIsStillImageFootageOnly(ItemPtr item); + + static bool CompareStillImageSize(ItemPtr item, const QSize& sz); + + static int64_t GetImageSequenceLimit(const QString &start_fn, int64_t start, bool up); QUndoCommand* command_; @@ -70,6 +79,8 @@ private: QStringList invalid_files_; + QList image_sequence_ignore_files_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/footagecombobox/footagecombobox.cpp b/app/widget/footagecombobox/footagecombobox.cpp index a55d96727..455dd0230 100644 --- a/app/widget/footagecombobox/footagecombobox.cpp +++ b/app/widget/footagecombobox/footagecombobox.cpp @@ -1,4 +1,4 @@ -/*** +/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team @@ -98,14 +98,14 @@ void FootageComboBox::TraverseFolder(const Folder *f, QMenu *m) Footage* footage = static_cast(child); - if (!only_show_ready_footage_ || footage->status() == Footage::kReady) { + if (footage->IsValid() || !only_show_ready_footage_) { Menu* stream_menu = new Menu(footage->name(), m); m->addMenu(stream_menu); foreach (StreamPtr stream, footage->streams()) { QAction* stream_action = stream_menu->addAction(FootageToString(stream.get())); stream_action->setData(QVariant::fromValue(stream)); - stream_action->setIcon(Stream::IconFromType(stream->type())); + stream_action->setIcon(stream->icon()); } } } diff --git a/app/widget/nodetableview/nodetabletraverser.cpp b/app/widget/nodetableview/nodetabletraverser.cpp index 77d92afb6..156409224 100644 --- a/app/widget/nodetableview/nodetabletraverser.cpp +++ b/app/widget/nodetableview/nodetabletraverser.cpp @@ -24,7 +24,7 @@ OLIVE_NAMESPACE_ENTER QVariant NodeTableTraverser::ProcessVideoFootage(StreamPtr stream, const rational &input_time) { - ImageStreamPtr video_stream = std::static_pointer_cast(stream); + VideoStreamPtr video_stream = std::static_pointer_cast(stream); return QVariant::fromValue(VideoParams(video_stream->width(), video_stream->height(), diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 9850a6eab..78e4542e2 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -44,7 +44,6 @@ Timeline::TrackType TrackTypeFromStreamType(Stream::Type stream_type) { switch (stream_type) { case Stream::kVideo: - case Stream::kImage: return Timeline::kTrackTypeVideo; case Stream::kAudio: return Timeline::kTrackTypeAudio; @@ -99,8 +98,12 @@ void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) // Check if Item is Footage if (item->type() == Item::kFootage) { - // If the Item is Footage, we can create a Ghost from it - dragged_footage_.append(DraggedFootage(static_cast(item), enabled_streams)); + Footage* f = static_cast(item); + + if (f->IsValid()) { + // If the Item is Footage, we can create a Ghost from it + dragged_footage_.append(DraggedFootage(f, enabled_streams)); + } } } @@ -237,7 +240,8 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); - if (stream->type() == Stream::kImage) { + if (stream->type() == Stream::kVideo + && std::static_pointer_cast(stream)->video_type() == VideoStream::kVideoTypeStill) { // Stream is essentially length-less - we may use the default still image length in config, // or we may use another stream's length depending on the circumstance contains_image_stream = true; @@ -414,7 +418,6 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) switch (footage_stream->type()) { case Stream::kVideo: - case Stream::kImage: { VideoInput* video_input = new VideoInput(); video_input->SetFootage(footage_stream); diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 5ca81ae1e..a2a9d7b1f 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -84,8 +84,7 @@ void FootageViewerWidget::SetFootage(Footage *footage) audio_stream = std::static_pointer_cast(s); } - if (!video_stream - && (s->type() == Stream::kVideo || s->type() == Stream::kImage)) { + if (!video_stream && s->type() == Stream::kVideo) { video_stream = std::static_pointer_cast(s); } diff --git a/app/widget/viewer/gizmotraverser.cpp b/app/widget/viewer/gizmotraverser.cpp index 85ca2fbec..b28128d7f 100644 --- a/app/widget/viewer/gizmotraverser.cpp +++ b/app/widget/viewer/gizmotraverser.cpp @@ -26,7 +26,7 @@ QVariant GizmoTraverser::ProcessVideoFootage(StreamPtr stream, const rational &i { Q_UNUSED(input_time) - ImageStreamPtr image_stream = std::static_pointer_cast(stream); + VideoStreamPtr image_stream = std::static_pointer_cast(stream); return QSize(image_stream->width(), image_stream->height()); }