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

Image streams were initially separated from video streams, but they're now
joined with a parameter defining if they're a still image, image sequence,
or regular video. The image sequence import process has also improved so
that if images from the same sequence are imported too, they'll either be
ignored or the user won't be asked again for those if they should be an
image sequence (fixes #1193). Also shifts decoder "probe" process to return
an item, useful if the decoder returns a non-footage item.
This commit is contained in:
itsmattkc
2020-09-26 19:45:07 +10:00
parent 85abd6c6b4
commit 6db591e89f
37 changed files with 613 additions and 779 deletions
+18 -20
View File
@@ -98,23 +98,20 @@ QVector<DecoderPtr> ReceiveListOfAllDecoders() {
return decoders;
}
bool Decoder::ProbeMedia(Footage *f, const QAtomicInt* cancelled)
ItemPtr Decoder::ProbeMedia(const QString &filename, const QAtomicInt* cancelled)
{
// Check for a valid filename
if (f->filename().isEmpty()) {
if (filename.isEmpty()) {
qWarning() << "Tried to probe media with an empty filename";
return false;
return nullptr;
}
// Check file exists
if (!QFileInfo::exists(f->filename())) {
qWarning() << "Tried to probe file that doesn't exist:" << f->filename();
return false;
if (!QFileInfo::exists(filename)) {
qWarning() << "Tried to probe file that doesn't exist:" << filename;
return nullptr;
}
// Reset Footage state for probing
f->Clear();
// Create list to iterate through
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
@@ -122,30 +119,31 @@ bool Decoder::ProbeMedia(Footage *f, const QAtomicInt* cancelled)
for (int i=0;i<decoder_list.size();i++) {
if (cancelled && *cancelled) {
return false;
return nullptr;
}
DecoderPtr decoder = decoder_list.at(i);
if (decoder->Probe(f, cancelled)) {
ItemPtr item = decoder->Probe(filename, cancelled);
// We found a Decoder, so we can set this media as valid
f->set_status(Footage::kReady);
if (item) {
if (item->type() == Item::kFootage) {
// Attach the successful Decoder to this Footage object
FootagePtr footage = std::static_pointer_cast<Footage>(item);
footage->set_decoder(decoder->id());
footage->SetValid();
}
// Attach the successful Decoder to this Footage object
f->set_decoder(decoder->id());
// FIXME: Cache the results so we don't have to probe if this media is added a second time
return true;
return item;
}
}
// We aren't able to use this Footage
f->set_status(Footage::kInvalid);
f->set_decoder(QString());
return false;
return nullptr;
}
DecoderPtr Decoder::CreateFromID(const QString &id)
+8 -8
View File
@@ -102,7 +102,7 @@ public:
* TRUE if the Decoder was able to decode this file. FALSE if not. This function should have filled the Footage
* object with metadata if it returns TRUE. Otherwise, the Footage object should be untouched.
*/
virtual bool Probe(Footage* f, const QAtomicInt* cancelled) = 0;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const = 0;
/**
* @brief Open media/allocate memory
@@ -199,7 +199,7 @@ public:
*
* TRUE if a Decoder was successfully able to parse and probe this file. FALSE if not.
*/
static bool ProbeMedia(Footage* f, const QAtomicInt *cancelled);
static ItemPtr ProbeMedia(const QString& filename, const QAtomicInt *cancelled);
/**
* @brief Create a Decoder instance using a Decoder ID
@@ -232,6 +232,12 @@ public:
*/
bool HasConformedVersion(const AudioParams& params);
static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number);
static int GetImageSequenceDigitCount(const QString& filename);
static int64_t GetImageSequenceIndex(const QString& filename);
signals:
/**
* @brief While indexing, this signal will provide progress as a percentage (0-100 inclusive) if
@@ -249,12 +255,6 @@ protected:
QString GetIndexFilename();
static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number);
static int GetImageSequenceDigitCount(const QString& filename);
static int64_t GetImageSequenceIndex(const QString& filename);
bool open_;
private:
+29 -37
View File
@@ -85,7 +85,7 @@ bool FFmpegDecoder::Open()
return false;
}
if (stream()->type() == Stream::kImage || stream()->type() == Stream::kVideo) {
if (stream()->type() == Stream::kVideo) {
// Get an Olive compatible AVPixelFormat
src_pix_fmt_ = static_cast<AVPixelFormat>(our_instance->stream()->codecpar->format);
ideal_pix_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(src_pix_fmt_);
@@ -130,12 +130,12 @@ bool FFmpegDecoder::Open()
FramePtr FFmpegDecoder::RetrieveStillImage(const rational &timecode, const int &divider)
{
// This is a still image
ImageStreamPtr is = std::static_pointer_cast<ImageStream>(stream());
VideoStreamPtr is = std::static_pointer_cast<VideoStream>(stream());
QString img_filename = stream()->footage()->filename();
// If it's an image sequence, we'll probably need to transform the filename
if (stream()->type() == Stream::kVideo) {
if (is->video_type() == VideoStream::kVideoTypeImageSequence) {
int64_t ts = std::static_pointer_cast<VideoStream>(stream())->get_time_in_timebase_units(timecode);
img_filename = TransformImageSequenceFileName(stream()->footage()->filename(), ts);
@@ -173,14 +173,14 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid
return nullptr;
}
if (stream()->type() != Stream::kImage && stream()->type() != Stream::kVideo) {
if (stream()->type() != Stream::kVideo) {
return nullptr;
}
ImageStreamPtr is = std::static_pointer_cast<ImageStream>(stream());
VideoStreamPtr vs = std::static_pointer_cast<VideoStream>(stream());
if (stream()->type() == Stream::kImage
|| std::static_pointer_cast<VideoStream>(stream())->is_image_sequence()) {
if (vs->video_type() == VideoStream::kVideoTypeStill
|| vs->video_type() == VideoStream::kVideoTypeImageSequence) {
return RetrieveStillImage(timecode, divider);
@@ -188,8 +188,6 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid
FFmpegFramePool::ElementPtr return_frame = nullptr;
VideoStreamPtr vs = std::static_pointer_cast<VideoStream>(stream());
int64_t target_ts = vs->get_time_in_timebase_units(timecode);
FFmpegDecoderInstance* working_instance = nullptr;
@@ -450,26 +448,21 @@ bool FFmpegDecoder::SupportsAudio()
return true;
}
bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
ItemPtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
if (open_) {
qWarning() << "Probe must be called while the Decoder is closed";
return false;
}
// Variable for receiving errors from FFmpeg
int error_code;
// Result to return
bool result = false;
FootagePtr footage = nullptr;
// Convert QString to a C string
QByteArray ba = f->filename().toUtf8();
const char* filename = ba.constData();
QByteArray ba = filename.toUtf8();
const char* filename_c = ba.constData();
// Open file in a format context
AVFormatContext* fmt_ctx = nullptr;
error_code = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
error_code = avformat_open_input(&fmt_ctx, filename_c, nullptr, nullptr);
// Handle format context error
if (error_code == 0) {
@@ -502,7 +495,7 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
AVFrame* frame = av_frame_alloc();
{
FFmpegDecoderInstance instance(filename, i);
FFmpegDecoderInstance instance(filename_c, i);
// Read first frame and retrieve some metadata
if (instance.GetFrame(pkt, frame) >= 0) {
@@ -548,26 +541,24 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
av_packet_free(&pkt);
}
ImageStreamPtr image_stream;
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
if (image_is_still) {
image_stream = std::make_shared<ImageStream>();
video_stream->set_video_type(VideoStream::kVideoTypeStill);
} else {
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
video_stream->set_video_type(VideoStream::kVideoTypeVideo);
video_stream->set_frame_rate(frame_rate);
video_stream->set_start_time(avstream->start_time);
image_stream = video_stream;
}
image_stream->set_width(avstream->codecpar->width);
image_stream->set_height(avstream->codecpar->height);
image_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast<AVPixelFormat>(avstream->codecpar->format))));
image_stream->set_interlacing(interlacing);
image_stream->set_pixel_aspect_ratio(pixel_aspect_ratio);
video_stream->set_width(avstream->codecpar->width);
video_stream->set_height(avstream->codecpar->height);
video_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast<AVPixelFormat>(avstream->codecpar->format))));
video_stream->set_interlacing(interlacing);
video_stream->set_pixel_aspect_ratio(pixel_aspect_ratio);
str = image_stream;
str = video_stream;
} else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && decoder) {
@@ -630,19 +621,20 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled)
}
if (found_valid_streams) {
// We actually have footage we can return instead of nullptr
footage = std::make_shared<Footage>();
// Copy streams over
foreach (StreamPtr stream, streams) {
f->add_stream(stream);
footage->add_stream(stream);
}
result = true;
}
}
// Free all memory
avformat_close_input(&fmt_ctx);
return result;
return footage;
}
void FFmpegDecoder::FFmpegError(int error_code)
@@ -834,8 +826,8 @@ FramePtr FFmpegDecoder::BuffersToNativeFrame(int divider, int width, int height,
copy->set_video_params(VideoParams(width,
height,
native_pix_fmt_,
std::static_pointer_cast<ImageStream>(stream())->pixel_aspect_ratio(),
std::static_pointer_cast<ImageStream>(stream())->interlacing(),
std::static_pointer_cast<VideoStream>(stream())->pixel_aspect_ratio(),
std::static_pointer_cast<VideoStream>(stream())->interlacing(),
divider));
copy->set_timestamp(ts);
copy->allocate();
+1 -1
View File
@@ -146,7 +146,7 @@ public:
// Destructor
virtual ~FFmpegDecoder() override;
virtual bool Probe(Footage *f, const QAtomicInt *cancelled) override;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
virtual bool Open() override;
virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override;
+11 -82
View File
@@ -45,86 +45,34 @@ QString OIIODecoder::id()
return QStringLiteral("oiio");
}
bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
ItemPtr OIIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
if (!FileTypeIsSupported(f->filename())) {
return false;
if (!FileTypeIsSupported(filename)) {
return nullptr;
}
std::string std_filename = f->filename().toStdString();
std::string std_filename = filename.toStdString();
auto in = OIIO::ImageInput::open(std_filename);
if (!in) {
return false;
return nullptr;
}
if (!strcmp(in->format_name(), "FFmpeg movie")) {
// If this is FFmpeg via OIIO, fall-through to our native FFmpeg decoder
return false;
return nullptr;
}
is_sequence_ = false;
FootagePtr footage = std::make_shared<Footage>();
// Heuristically determine whether this file is part of an image sequence or not
if (GetImageSequenceDigitCount(f->filename()) > 0) {
QSize dim(in->spec().width, in->spec().height);
int64_t ind = GetImageSequenceIndex(f->filename());
// Check if files around exist around it with that follow a sequence
QString previous_img_fn = TransformImageSequenceFileName(f->filename(), ind - 1);
QString next_img_fn = TransformImageSequenceFileName(f->filename(), ind + 1);
// GetImageDimensions will return a 0,0 size if the file doesn't exist, so it's safe to check
// both existence and matching size with this
if (GetImageDimensions(previous_img_fn) == dim || GetImageDimensions(next_img_fn) == dim) {
// We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer
// to the Core which will definitely be in the UI thread and block here until we get an answer from the user
QMetaObject::invokeMethod(Core::instance(),
"ConfirmImageSequence",
Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, is_sequence_),
Q_ARG(QString, f->filename()));
}
}
ImageStreamPtr image_stream;
if (is_sequence_) {
VideoStreamPtr video_stream = std::make_shared<VideoStream>();
image_stream = video_stream;
rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value<rational>();
video_stream->set_timebase(default_timebase);
video_stream->set_frame_rate(default_timebase.flipped());
video_stream->set_image_sequence(true);
int64_t seq_index = GetImageSequenceIndex(f->filename());
int64_t start_index = seq_index;
int64_t end_index = seq_index;
// Heuristic to find the first and last images (users can always override this later in FootagePropertiesDialog)
while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), start_index-1))) {
start_index--;
}
while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), end_index+1))) {
end_index++;
}
video_stream->set_start_time(start_index);
video_stream->set_duration(end_index - start_index + 1);
} else {
image_stream = std::make_shared<ImageStream>();
}
VideoStreamPtr image_stream = std::make_shared<VideoStream>();
image_stream->set_width(in->spec().width);
image_stream->set_height(in->spec().height);
image_stream->set_format(GetFormatFromOIIOBasetype(in->spec()));
image_stream->set_pixel_aspect_ratio(GetPixelAspectRatioFromOIIO(in->spec()));
image_stream->set_video_type(VideoStream::kVideoTypeStill);
// Images will always have just one stream
image_stream->set_index(0);
@@ -135,7 +83,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
image_stream->set_premultiplied_alpha(true);
// Get stats for this image and dump them into the Footage file
f->add_stream(image_stream);
footage->add_stream(image_stream);
// If we're here, we have a successful image open
in->close();
@@ -144,7 +92,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled)
OIIO::ImageInput::destroy(in);
#endif
return true;
return footage;
}
bool OIIODecoder::Open()
@@ -322,25 +270,6 @@ bool OIIODecoder::FileTypeIsSupported(const QString& fn)
return true;
}
QSize OIIODecoder::GetImageDimensions(const QString &fn)
{
QSize sz;
auto in = OIIO::ImageInput::open(fn.toStdString());
if (in) {
sz.setWidth(in->spec().width);
sz.setHeight(in->spec().height);
in->close();
#if OIIO_VERSION < 10903
OIIO::ImageInput::destroy(in);
#endif
}
return sz;
}
bool OIIODecoder::OpenImageHandler(const QString &fn)
{
image_ = OIIO::ImageInput::open(fn.toStdString());
+1 -5
View File
@@ -37,7 +37,7 @@ public:
virtual QString id() override;
virtual bool Probe(Footage *f, const QAtomicInt* cancelled) override;
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
virtual bool Open() override;
virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override;
@@ -62,8 +62,6 @@ private:
static bool FileTypeIsSupported(const QString& fn);
static QSize GetImageDimensions(const QString& fn);
bool OpenImageHandler(const QString& fn);
void CloseImageHandle();
@@ -72,8 +70,6 @@ private:
bool is_rgba_;
bool is_sequence_;
OIIO::ImageBuf* buffer_;
static QStringList supported_formats_;
+16 -2
View File
@@ -20,6 +20,8 @@
#include "otiodecoder.h"
#include <opentimelineio/timeline.h>
OLIVE_NAMESPACE_ENTER
OTIODecoder::OTIODecoder()
@@ -32,9 +34,21 @@ QString OTIODecoder::id()
return QStringLiteral("otio");
}
bool OTIODecoder::Probe(Footage* f, const QAtomicInt* cancelled)
ItemPtr OTIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const
{
return false;
if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) {
opentimelineio::v1_0::ErrorStatus es;
auto timeline = static_cast<opentimelineio::v1_0::Timeline*>(opentimelineio::v1_0::SerializableObjectWithMetadata::from_json_file(filename.toStdString(), &es));
if (es != opentimelineio::v1_0::ErrorStatus::OK) {
return nullptr;
}
qDebug() << "Found" << timeline->video_tracks().size() << "video tracks";
}
return nullptr;
}
OLIVE_NAMESPACE_EXIT
+4 -1
View File
@@ -33,7 +33,10 @@ public:
virtual QString id() override;
virtual bool Probe(Footage* f, const QAtomicInt* cancelled) override;
virtual bool Open() override {return false;}
virtual void Close() override {}
virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override;
};
@@ -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<ImageStream>(stream)));
stacked_widget_->addWidget(new VideoStreamProperties(std::static_pointer_cast<VideoStream>(stream)));
break;
case Stream::kAudio:
stacked_widget_->addWidget(new AudioStreamProperties(std::static_pointer_cast<AudioStream>(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;
}
}
@@ -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<VideoStream>(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<VideoStream*>(stream)->is_image_sequence());
}
VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream,
VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(VideoStreamPtr stream,
bool premultiplied,
QString colorspace,
VideoParams::Interlacing interlacing,
@@ -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_;
+3 -3
View File
@@ -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<ImageStream>(stream);
if (stream->type() == Stream::kVideo) {
VideoStreamPtr image_stream = std::static_pointer_cast<VideoStream>(stream);
// Current color config and space
hash.addData(image_stream->footage()->project()->color_manager()->GetConfigFilename().toUtf8());
+1 -2
View File
@@ -189,8 +189,7 @@ void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, N
StreamPtr s = v.data().value<StreamPtr>();
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;
@@ -51,6 +51,11 @@ QList<Footage *> FootageViewerPanel::GetSelectedFootage() const
void FootageViewerPanel::SetFootage(Footage *f)
{
if (!f->IsValid()) {
// Do nothing if footage is invalid
return;
}
static_cast<FootageViewerWidget*>(GetTimeBasedWidget())->SetFootage(f);
if (f) {
-2
View File
@@ -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
+5
View File
@@ -96,4 +96,9 @@ void AudioStream::append_conformed_version(const AudioParams &params)
emit ConformAppended(params);
}
QIcon AudioStream::icon() const
{
return icon::Audio;
}
OLIVE_NAMESPACE_EXIT
+2
View File
@@ -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);
+30 -92
View File
@@ -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<VideoStream>(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<VideoStream>(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<VideoStream>(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;i<streams_.size();i++) {
StreamPtr s = streams_.at(i);
switch (s->type()) {
case Stream::kVideo:
case Stream::kImage:
{
ImageStreamPtr vs = std::static_pointer_cast<ImageStream>(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<AudioStream>(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"));
}
}
+12 -33
View File
@@ -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<StreamPtr> streams_;
/**
* @brief Internal ready setting
*/
Status status_;
/**
* @brief Internal attached decoder ID
*/
QString decoder_;
bool valid_;
};
using FootagePtr = std::shared_ptr<Footage>;
-136
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#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
-137
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#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<ImageStream>;
OLIVE_NAMESPACE_EXIT
#endif // IMAGESTREAM_H
+1 -17
View File
@@ -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();
+3 -5
View File
@@ -28,6 +28,7 @@
#include <QXmlStreamWriter>
#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;
+87 -110
View File
@@ -23,21 +23,35 @@
#include <QFile>
#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;i<frame_index_.size();i++) {
int64_t this_ts = frame_index_.at(i);
reader->skipCurrentElement();
}
}
}
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<size_t>(index_file.size()) / sizeof(int64_t));
// Read frame index into vector
index_file.read(reinterpret_cast<char*>(frame_index_.data()),
index_file.size());
}
index_file.close();
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<const char*>(frame_index_.constData()),
frame_index_.size() * static_cast<int>(sizeof(int64_t)));
index_file.close();
return true;
}
return false;
}
*/
OLIVE_NAMESPACE_EXIT
+111 -14
View File
@@ -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<int64_t> frame_index_;
VideoType video_type_;
PixelFormat::Format format_;
rational pixel_aspect_ratio_;
rational frame_rate_;
int64_t start_time_;
+14 -18
View File
@@ -278,33 +278,29 @@ void Sequence::set_parameters_from_footage(const QList<Footage *> footage)
VideoStream* vs = static_cast<VideoStream*>(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<PixelFormat::Format>(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<ImageStream*>(s.get());
set_video_params(VideoParams(is->width(),
is->height(),
video_params().time_base(),
static_cast<PixelFormat::Format>(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<AudioStream*>(s.get());
+31
View File
@@ -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<ItemPtr> footage = this->get_items_of_type(Item::kFootage);
foreach (ItemPtr item, footage) {
foreach (StreamPtr s, std::static_pointer_cast<Footage>(item)->streams()) {
if (s->type() == Stream::kVideo) {
std::static_pointer_cast<VideoStream>(s)->ColorConfigChanged();
}
}
}
}
void Project::DefaultColorSpaceChanged()
{
QList<ItemPtr> footage = this->get_items_of_type(Item::kFootage);
foreach (ItemPtr item, footage) {
foreach (StreamPtr s, std::static_pointer_cast<Footage>(item)->streams()) {
if (s->type() == Stream::kVideo) {
std::static_pointer_cast<VideoStream>(s)->DefaultColorSpaceChanged();
}
}
}
}
OLIVE_NAMESPACE_EXIT
+5
View File
@@ -96,6 +96,11 @@ private:
QString cache_path_;
private slots:
void ColorConfigChanged();
void DefaultColorSpaceChanged();
};
using ProjectPtr = std::shared_ptr<Project>;
+1 -1
View File
@@ -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<ImageStream>(stream);
VideoStreamPtr video_stream = std::static_pointer_cast<VideoStream>(stream);
// Set up OCIO context
QString colorspace_match = video_stream->get_colorspace_match_string();
+2 -2
View File
@@ -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<ImageStream>(stream);
rational time_match = (stream->type() == Stream::kImage) ? rational() : input_time;
VideoStreamPtr video_stream = std::static_pointer_cast<VideoStream>(stream);
rational time_match = (video_stream->video_type() == VideoStream::kVideoTypeStill) ? 0 : input_time;
QString colorspace_match = video_stream->get_colorspace_match_string();
QVariant value;
+175 -18
View File
@@ -23,8 +23,8 @@
#include <QDir>
#include <QFileInfo>
#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<import.size(); i++) {
if (IsCancelled()) {
break;
}
const QFileInfo& file_info = import.at(i);
// Check if this file is a directory
if (file_info.isDir()) {
@@ -107,30 +109,35 @@ void ProjectImportTask::Import(Folder *folder, const QFileInfoList &import, int
} else {
FootagePtr f = std::make_shared<Footage>();
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<Footage>(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<Footage>(item);
VideoStreamPtr video_stream = std::static_pointer_cast<VideoStream>(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; i<info_list.size(); i++) {
if (info_list.at(i).absoluteFilePath() == entry_fn) {
if (is_sequence) {
info_list.removeAt(i);
}
break;
}
}
} else {
image_sequence_ignore_files_.append(entry_fn);
}
}
if (is_sequence) {
// User has confirmed it is a still image, let's set it accordingly.
video_stream->set_video_type(VideoStream::kVideoTypeVideo);
rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value<rational>();
video_stream->set_timebase(default_timebase);
video_stream->set_frame_rate(default_timebase.flipped());
video_stream->set_image_sequence(true);
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<Footage>(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<VideoStream>(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<Footage>(item);
VideoStreamPtr video_stream = std::static_pointer_cast<VideoStream>(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
+12 -1
View File
@@ -24,6 +24,7 @@
#include <QFileInfoList>
#include <QUndoCommand>
#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<QString> image_sequence_ignore_files_;
};
OLIVE_NAMESPACE_EXIT
@@ -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<Footage*>(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());
}
}
}
@@ -24,7 +24,7 @@ OLIVE_NAMESPACE_ENTER
QVariant NodeTableTraverser::ProcessVideoFootage(StreamPtr stream, const rational &input_time)
{
ImageStreamPtr video_stream = std::static_pointer_cast<ImageStream>(stream);
VideoStreamPtr video_stream = std::static_pointer_cast<VideoStream>(stream);
return QVariant::fromValue(VideoParams(video_stream->width(),
video_stream->height(),
+8 -5
View File
@@ -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<Footage*>(item), enabled_streams));
Footage* f = static_cast<Footage*>(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<VideoStream>(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);
+1 -2
View File
@@ -84,8 +84,7 @@ void FootageViewerWidget::SetFootage(Footage *footage)
audio_stream = std::static_pointer_cast<AudioStream>(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<VideoStream>(s);
}
+1 -1
View File
@@ -26,7 +26,7 @@ QVariant GizmoTraverser::ProcessVideoFootage(StreamPtr stream, const rational &i
{
Q_UNUSED(input_time)
ImageStreamPtr image_stream = std::static_pointer_cast<ImageStream>(stream);
VideoStreamPtr image_stream = std::static_pointer_cast<VideoStream>(stream);
return QSize(image_stream->width(), image_stream->height());
}