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()); }