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