From 4942970c0218b2c1c41902924edb8399ba9813f7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 5 May 2021 19:03:03 +1000 Subject: [PATCH] export: fixed many export formats including still image and image sequence export Still incomplete, but does a lot towards #1345 --- app/codec/encoder.cpp | 95 +++++++++- app/codec/encoder.h | 34 +++- app/codec/exportcodec.cpp | 55 ++---- app/codec/exportcodec.h | 8 +- app/codec/exportformat.cpp | 85 ++++++--- app/codec/exportformat.h | 9 +- app/codec/ffmpeg/ffmpegencoder.cpp | 65 +++++++ app/codec/ffmpeg/ffmpegencoder.h | 2 + app/codec/oiio/CMakeLists.txt | 2 + app/codec/oiio/oiioencoder.cpp | 75 ++++++++ app/codec/oiio/oiioencoder.h | 47 +++++ app/common/qtutils.cpp | 20 +++ app/common/qtutils.h | 3 + app/common/rational.cpp | 9 +- app/common/rational.h | 1 + app/dialog/export/codec/h264section.cpp | 20 ++- app/dialog/export/codec/h264section.h | 16 +- app/dialog/export/codec/imagesection.cpp | 16 +- app/dialog/export/codec/imagesection.h | 26 ++- app/dialog/export/export.cpp | 212 ++++++++++++++++------- app/dialog/export/export.h | 13 ++ app/dialog/export/exportaudiotab.cpp | 11 ++ app/dialog/export/exportaudiotab.h | 3 + app/dialog/export/exportvideotab.cpp | 64 ++++++- app/dialog/export/exportvideotab.h | 45 +++-- app/task/export/exportparams.cpp | 6 +- app/task/export/exportparams.h | 6 +- 27 files changed, 775 insertions(+), 173 deletions(-) create mode 100644 app/codec/oiio/oiioencoder.cpp create mode 100644 app/codec/oiio/oiioencoder.h diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 2cf1b8749..ba109d678 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -22,10 +22,15 @@ #include +#include "common/timecodefunctions.h" #include "ffmpeg/ffmpegencoder.h" +#include "oiio/oiioencoder.h" namespace olive { +const QRegularExpression Encoder::kImageSequenceContainsDigits = QRegularExpression(QStringLiteral("\\[[#]+\\]")); +const QRegularExpression Encoder::kImageSequenceRemoveDigits = QRegularExpression(QStringLiteral("[\\-\\.\\ \\_]?\\[[#]+\\]")); + Encoder::Encoder(const EncodingParams ¶ms) : params_(params) { @@ -36,6 +41,47 @@ const EncodingParams &Encoder::params() const return params_; } +QString Encoder::GetFilenameForFrame(const rational &frame) +{ + if (params().video_is_image_sequence()) { + // Transform! + int64_t frame_index = Timecode::time_to_timestamp(frame, params().video_params().frame_rate_as_time_base()); + int digits = GetImageSequencePlaceholderDigitCount(params().filename()); + QString frame_index_str = QStringLiteral("%1").arg(frame_index, digits, 10, QChar('0')); + + QString f = params_.filename(); + f.replace(kImageSequenceContainsDigits, frame_index_str); + return f; + } else { + // Keep filename + return params_.filename(); + } +} + +int Encoder::GetImageSequencePlaceholderDigitCount(const QString &filename) +{ + int start = filename.indexOf(kImageSequenceContainsDigits); + int digit_count = 0; + for (int i=start+1; iwriteEndElement(); // audio } -Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) +Encoder* Encoder::CreateFromID(Type id, const EncodingParams& params) { - Q_UNUSED(id) + switch (id) { + case kEncoderTypeNone: + break; + case kEncoderTypeFFmpeg: + return new FFmpegEncoder(params); + case kEncoderTypeOIIO: + return new OIIOEncoder(params); + } - return new FFmpegEncoder(params); + return nullptr; +} + +Encoder::Type Encoder::GetTypeFromFormat(ExportFormat::Format f) +{ + switch (f) { + case ExportFormat::kFormatDNxHD: + case ExportFormat::kFormatMatroska: + case ExportFormat::kFormatQuickTime: + case ExportFormat::kFormatMPEG4: + case ExportFormat::kFormatWAV: + case ExportFormat::kFormatAIFF: + case ExportFormat::kFormatMP3: + case ExportFormat::kFormatFLAC: + case ExportFormat::kFormatOgg: + case ExportFormat::kFormatWebM: + return kEncoderTypeFFmpeg; + case ExportFormat::kFormatOpenEXR: + case ExportFormat::kFormatPNG: + case ExportFormat::kFormatTIFF: + return kEncoderTypeOIIO; + case ExportFormat::kFormatCount: + break; + } + + return kEncoderTypeNone; +} + +Encoder *Encoder::CreateFromFormat(ExportFormat::Format f, const EncodingParams ¶ms) +{ + return CreateFromID(GetTypeFromFormat(f), params); +} + +QStringList Encoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const +{ + return QStringList(); } } diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 70b98c28a..f8693aa26 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -22,6 +22,7 @@ #define ENCODER_H #include +#include #include #include @@ -53,6 +54,10 @@ public: void set_video_buffer_size(const int64_t& sz); void set_video_threads(const int& threads); void set_video_pix_fmt(const QString& s); + void set_video_is_image_sequence(bool s) + { + video_is_image_sequence_ = s; + } const QString& filename() const; @@ -66,6 +71,10 @@ public: const int64_t& video_buffer_size() const; const int& video_threads() const; const QString& video_pix_fmt() const; + bool video_is_image_sequence() const + { + return video_is_image_sequence_; + } bool audio_enabled() const; const ExportCodec::Codec &audio_codec() const; @@ -99,6 +108,7 @@ private: int64_t video_buffer_size_; int video_threads_; QString video_pix_fmt_; + bool video_is_image_sequence_; bool audio_enabled_; ExportCodec::Codec audio_codec_; @@ -115,6 +125,12 @@ class Encoder : public QObject public: Encoder(const EncodingParams& params); + enum Type { + kEncoderTypeNone = -1, + kEncoderTypeFFmpeg, + kEncoderTypeOIIO + }; + /** * @brief Create a Encoder instance using a Encoder ID * @@ -122,7 +138,13 @@ public: * * A Encoder instance or nullptr if a Decoder with this ID does not exist */ - static Encoder *CreateFromID(const QString& id, const EncodingParams ¶ms); + static Encoder *CreateFromID(Type id, const EncodingParams ¶ms); + + static Type GetTypeFromFormat(ExportFormat::Format f); + + static Encoder *CreateFromFormat(ExportFormat::Format f, const EncodingParams ¶ms); + + virtual QStringList GetPixelFormatsForCodec(ExportCodec::Codec c) const; const EncodingParams& params() const; @@ -136,6 +158,16 @@ public: return error_; } + QString GetFilenameForFrame(const rational& frame); + + static int GetImageSequencePlaceholderDigitCount(const QString& filename); + + static bool FilenameContainsDigitPlaceholder(const QString &filename); + static QString FilenameRemoveDigitPlaceholder(QString filename); + + static const QRegularExpression kImageSequenceContainsDigits; + static const QRegularExpression kImageSequenceRemoveDigits; + public slots: virtual bool Open() = 0; diff --git a/app/codec/exportcodec.cpp b/app/codec/exportcodec.cpp index 618d87de4..f4955743c 100644 --- a/app/codec/exportcodec.cpp +++ b/app/codec/exportcodec.cpp @@ -52,6 +52,14 @@ QString ExportCodec::GetCodecName(ExportCodec::Codec c) return tr("AAC"); case kCodecPCM: return tr("PCM (Uncompressed)"); + case kCodecFLAC: + return tr("FLAC"); + case kCodecOpus: + return tr("Opus"); + case kCodecVorbis: + return tr("Vorbis"); + case kCodecVP9: + return tr("VP9"); case kCodecCount: break; } @@ -70,6 +78,10 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) case kCodecMP3: case kCodecAAC: case kCodecPCM: + case kCodecVorbis: + case kCodecOpus: + case kCodecFLAC: + case kCodecVP9: return false; case kCodecOpenEXR: case kCodecPNG: @@ -82,47 +94,4 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) return false; } -QStringList ExportCodec::GetPixelFormatsForCodec(ExportCodec::Codec c) -{ - QStringList pix_fmts; - - AVCodec* codec_info = nullptr; - - switch (c) { - case kCodecH264: - codec_info = avcodec_find_encoder(AV_CODEC_ID_H264); - break; - case kCodecDNxHD: - codec_info = avcodec_find_encoder(AV_CODEC_ID_DNXHD); - break; - case kCodecProRes: - codec_info = avcodec_find_encoder(AV_CODEC_ID_PRORES); - break; - case kCodecH265: - codec_info = avcodec_find_encoder(AV_CODEC_ID_HEVC); - break; - case kCodecOpenEXR: - case kCodecPNG: - case kCodecTIFF: - // FIXME: Add these in (these will most likely use an OIIOEncoder which doesn't exist yet) - break; - case kCodecMP2: - case kCodecMP3: - case kCodecAAC: - case kCodecPCM: - case kCodecCount: - // These are audio or invalid codecs and therefore have no pixel formats - break; - } - - if (codec_info) { - for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { - const char* pix_fmt_name = av_get_pix_fmt_name(codec_info->pix_fmts[i]); - pix_fmts.append(pix_fmt_name); - } - } - - return pix_fmts; -} - } diff --git a/app/codec/exportcodec.h b/app/codec/exportcodec.h index 1eea15f7e..70e5cb74b 100644 --- a/app/codec/exportcodec.h +++ b/app/codec/exportcodec.h @@ -33,6 +33,7 @@ class ExportCodec : public QObject Q_OBJECT public: enum Codec { + // Video codecs kCodecDNxHD, kCodecH264, kCodecH265, @@ -40,11 +41,16 @@ public: kCodecPNG, kCodecProRes, kCodecTIFF, + kCodecVP9, + // Audio codecs kCodecMP2, kCodecMP3, kCodecAAC, kCodecPCM, + kCodecOpus, + kCodecVorbis, + kCodecFLAC, kCodecCount }; @@ -53,8 +59,6 @@ public: static bool IsCodecAStillImage(Codec c); - static QStringList GetPixelFormatsForCodec(Codec c); - }; } diff --git a/app/codec/exportformat.cpp b/app/codec/exportformat.cpp index 1fa641faf..6b5ec1784 100644 --- a/app/codec/exportformat.cpp +++ b/app/codec/exportformat.cpp @@ -20,6 +20,8 @@ #include "exportformat.h" +#include "encoder.h" + namespace olive { QString ExportFormat::GetName(olive::ExportFormat::Format f) @@ -39,6 +41,19 @@ QString ExportFormat::GetName(olive::ExportFormat::Format f) return tr("TIFF"); case kFormatQuickTime: return tr("QuickTime"); + case kFormatWAV: + return tr("Wave Audio"); + case kFormatAIFF: + return tr("AIFF"); + case kFormatMP3: + return tr("MP3"); + case kFormatFLAC: + return tr("FLAC"); + case kFormatOgg: + return tr("Ogg"); + case kFormatWebM: + return tr("WebM"); + case kFormatCount: break; } @@ -63,25 +78,18 @@ QString ExportFormat::GetExtension(ExportFormat::Format f) return QStringLiteral("tiff"); case kFormatQuickTime: return QStringLiteral("mov"); - case kFormatCount: - break; - } - - return QString(); -} - -QString ExportFormat::GetEncoder(ExportFormat::Format f) -{ - switch (f) { - case kFormatDNxHD: - case kFormatMatroska: - case kFormatQuickTime: - case kFormatMPEG4: - return QStringLiteral("ffmpeg"); - case kFormatOpenEXR: - case kFormatPNG: - case kFormatTIFF: - return QStringLiteral("oiio"); + case kFormatWAV: + return QStringLiteral("wav"); + case kFormatAIFF: + return QStringLiteral("aiff"); + case kFormatMP3: + return QStringLiteral("mp3"); + case kFormatFLAC: + return QStringLiteral("flac"); + case kFormatOgg: + return QStringLiteral("ogg"); + case kFormatWebM: + return QStringLiteral("webm"); case kFormatCount: break; } @@ -106,6 +114,14 @@ QList ExportFormat::GetVideoCodecs(ExportFormat::Format f) return {ExportCodec::kCodecTIFF}; case kFormatQuickTime: return {ExportCodec::kCodecH264, ExportCodec::kCodecH265, ExportCodec::kCodecProRes}; + case kFormatWebM: + return {ExportCodec::kCodecVP9}; + case kFormatOgg: + case kFormatWAV: + case kFormatAIFF: + case kFormatMP3: + case kFormatFLAC: + return {}; case kFormatCount: break; } @@ -119,15 +135,31 @@ QList ExportFormat::GetAudioCodecs(ExportFormat::Format f) case kFormatDNxHD: return {ExportCodec::kCodecPCM}; case kFormatMatroska: - return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus}; case kFormatMPEG4: return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; case kFormatQuickTime: return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; + case kFormatWebM: + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus}; case kFormatOpenEXR: case kFormatPNG: case kFormatTIFF: return {}; + + + case kFormatWAV: + return {ExportCodec::kCodecPCM}; + case kFormatAIFF: + return {ExportCodec::kCodecPCM}; + case kFormatMP3: + return {ExportCodec::kCodecMP3}; + case kFormatFLAC: + return {ExportCodec::kCodecFLAC}; + case kFormatOgg: + return {ExportCodec::kCodecOpus, ExportCodec::kCodecVorbis, ExportCodec::kCodecPCM}; + + case kFormatCount: break; } @@ -135,4 +167,17 @@ QList ExportFormat::GetAudioCodecs(ExportFormat::Format f) return {}; } +QStringList ExportFormat::GetPixelFormatsForCodec(ExportFormat::Format f, ExportCodec::Codec c) +{ + Encoder* e = Encoder::CreateFromFormat(f, EncodingParams()); + QStringList list; + + if (e) { + list = e->GetPixelFormatsForCodec(c); + delete e; + } + + return list; +} + } diff --git a/app/codec/exportformat.h b/app/codec/exportformat.h index 7a454c67d..50cf73685 100644 --- a/app/codec/exportformat.h +++ b/app/codec/exportformat.h @@ -41,16 +41,23 @@ public: kFormatQuickTime, kFormatPNG, kFormatTIFF, + kFormatWAV, + kFormatAIFF, + kFormatMP3, + kFormatFLAC, + kFormatOgg, + kFormatWebM, kFormatCount }; static QString GetName(Format f); static QString GetExtension(Format f); - static QString GetEncoder(Format f); static QList GetVideoCodecs(ExportFormat::Format f); static QList GetAudioCodecs(ExportFormat::Format f); + static QStringList GetPixelFormatsForCodec(Format f, ExportCodec::Codec c); + }; } diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index fe7d77cf6..440c7d7c2 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -44,6 +44,59 @@ FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) : { } +QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const +{ + QStringList pix_fmts; + + AVCodec* codec_info = nullptr; + + switch (c) { + case ExportCodec::kCodecH264: + codec_info = avcodec_find_encoder(AV_CODEC_ID_H264); + break; + case ExportCodec::kCodecDNxHD: + codec_info = avcodec_find_encoder(AV_CODEC_ID_DNXHD); + break; + case ExportCodec::kCodecProRes: + codec_info = avcodec_find_encoder(AV_CODEC_ID_PRORES); + break; + case ExportCodec::kCodecH265: + codec_info = avcodec_find_encoder(AV_CODEC_ID_HEVC); + break; + case ExportCodec::kCodecVP9: + codec_info = avcodec_find_encoder(AV_CODEC_ID_VP9); + break; + case ExportCodec::kCodecOpenEXR: + codec_info = avcodec_find_encoder(AV_CODEC_ID_EXR); + break; + case ExportCodec::kCodecPNG: + codec_info = avcodec_find_encoder(AV_CODEC_ID_PNG); + break; + case ExportCodec::kCodecTIFF: + codec_info = avcodec_find_encoder(AV_CODEC_ID_TIFF); + break; + case ExportCodec::kCodecMP2: + case ExportCodec::kCodecMP3: + case ExportCodec::kCodecAAC: + case ExportCodec::kCodecPCM: + case ExportCodec::kCodecFLAC: + case ExportCodec::kCodecOpus: + case ExportCodec::kCodecVorbis: + case ExportCodec::kCodecCount: + // These are audio or invalid codecs and therefore have no pixel formats + break; + } + + if (codec_info) { + for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { + const char* pix_fmt_name = av_get_pix_fmt_name(codec_info->pix_fmts[i]); + pix_fmts.append(pix_fmt_name); + } + } + + return pix_fmts; +} + bool FFmpegEncoder::Open() { if (open_) { @@ -447,6 +500,18 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV case ExportCodec::kCodecPCM: codec_id = AV_CODEC_ID_PCM_S16LE; break; + case ExportCodec::kCodecVP9: + codec_id = AV_CODEC_ID_VP9; + break; + case ExportCodec::kCodecOpus: + codec_id = AV_CODEC_ID_OPUS; + break; + case ExportCodec::kCodecVorbis: + codec_id = AV_CODEC_ID_VORBIS; + break; + case ExportCodec::kCodecFLAC: + codec_id = AV_CODEC_ID_FLAC; + break; case ExportCodec::kCodecCount: break; } diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 4cf98fb63..9d87e1a24 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -38,6 +38,8 @@ class FFmpegEncoder : public Encoder public: FFmpegEncoder(const EncodingParams ¶ms); + virtual QStringList GetPixelFormatsForCodec(ExportCodec::Codec c) const override; + virtual bool Open() override; virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override; diff --git a/app/codec/oiio/CMakeLists.txt b/app/codec/oiio/CMakeLists.txt index a0e7cb257..c4d407fe6 100644 --- a/app/codec/oiio/CMakeLists.txt +++ b/app/codec/oiio/CMakeLists.txt @@ -18,5 +18,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} codec/oiio/oiiodecoder.cpp codec/oiio/oiiodecoder.h + codec/oiio/oiioencoder.cpp + codec/oiio/oiioencoder.h PARENT_SCOPE ) diff --git a/app/codec/oiio/oiioencoder.cpp b/app/codec/oiio/oiioencoder.cpp new file mode 100644 index 000000000..917b6df23 --- /dev/null +++ b/app/codec/oiio/oiioencoder.cpp @@ -0,0 +1,75 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "oiioencoder.h" + +#include "common/oiioutils.h" + +namespace olive { + +OIIOEncoder::OIIOEncoder(const EncodingParams ¶ms) : + Encoder(params) +{ + +} + +bool OIIOEncoder::Open() +{ + return true; +} + +bool OIIOEncoder::WriteFrame(FramePtr frame, rational time) +{ + std::string filename = GetFilenameForFrame(time).toStdString(); + + auto output = OIIO::ImageOutput::create(filename); + if (!output) { + return false; + } + + OIIO::TypeDesc type = OIIOUtils::GetOIIOBaseTypeFromFormat(frame->format()); + OIIO::ImageSpec spec(frame->width(), frame->height(), frame->channel_count(), type); + + if (!output->open(filename, spec)) { + return false; + } + + if (!output->write_image(type, frame->data(), OIIO::AutoStride, frame->linesize_bytes())) { + return false; + } + + if (!output->close()) { + return false; + } + + return true; +} + +void OIIOEncoder::WriteAudio(AudioParams pcm_info, QIODevice *file) +{ + // Do nothing +} + +void OIIOEncoder::Close() +{ + // Do nothing +} + +} diff --git a/app/codec/oiio/oiioencoder.h b/app/codec/oiio/oiioencoder.h new file mode 100644 index 000000000..d15a9ec95 --- /dev/null +++ b/app/codec/oiio/oiioencoder.h @@ -0,0 +1,47 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OIIOENCODER_H +#define OIIOENCODER_H + +#include "codec/encoder.h" + +namespace olive { + +class OIIOEncoder : public Encoder +{ + Q_OBJECT +public: + OIIOEncoder(const EncodingParams ¶ms); + +public slots: + virtual bool Open() override; + + virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override; + virtual void WriteAudio(olive::AudioParams pcm_info, + QIODevice *file) override; + + virtual void Close() override; + +}; + +} + +#endif // OIIOENCODER_H diff --git a/app/common/qtutils.cpp b/app/common/qtutils.cpp index 78c31490a..515a2bf67 100644 --- a/app/common/qtutils.cpp +++ b/app/common/qtutils.cpp @@ -38,4 +38,24 @@ QFrame *QtUtils::CreateHorizontalLine() return horizontal_line; } +int QtUtils::MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString &title, const QString &message, QMessageBox::StandardButtons buttons) +{ + QMessageBox b(parent); + b.setIcon(icon); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(title); + b.setText(message); + + uint mask = QMessageBox::FirstButton; + while (mask <= QMessageBox::LastButton) { + uint sb = buttons & mask; + if (sb) { + b.addButton(static_cast(sb)); + } + mask <<= 1; + } + + return b.exec(); +} + } diff --git a/app/common/qtutils.h b/app/common/qtutils.h index 5bcd7b1af..e50f35f07 100644 --- a/app/common/qtutils.h +++ b/app/common/qtutils.h @@ -29,6 +29,7 @@ #include #include +#include #include "common/define.h" @@ -47,6 +48,8 @@ public: static QFrame* CreateHorizontalLine(); + static int MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString& title, const QString& message, QMessageBox::StandardButtons buttons = QMessageBox::Ok); + }; } diff --git a/app/common/rational.cpp b/app/common/rational.cpp index f835fa904..8d12a69a1 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -135,7 +135,14 @@ opentime::RationalTime rational::toRationalTime(double framerate) const rational rational::flipped() const { - return rational(denom_, numer_); + rational r = *this; + r.flip(); + return r; +} + +void rational::flip() +{ + std::swap(denom_, numer_); } bool rational::isNull() const diff --git a/app/common/rational.h b/app/common/rational.h index 673988eea..6179b029e 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -100,6 +100,7 @@ public: // Produce "flipped" version rational flipped() const; + void flip(); // Returns whether the rational is valid but equal to zero or not // diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp index 93b82c3f0..101bf388f 100644 --- a/app/dialog/export/codec/h264section.cpp +++ b/app/dialog/export/codec/h264section.cpp @@ -31,6 +31,11 @@ namespace olive { H264Section::H264Section(QWidget *parent) : + H264Section(H264CRFSection::kDefaultH264CRF, parent) +{ +} + +H264Section::H264Section(int default_crf, QWidget *parent) : CodecSection(parent) { QGridLayout* layout = new QGridLayout(this); @@ -54,7 +59,7 @@ H264Section::H264Section(QWidget *parent) : compression_method_stack_ = new QStackedWidget(); layout->addWidget(compression_method_stack_, row, 0, 1, 2); - crf_section_ = new H264CRFSection(); + crf_section_ = new H264CRFSection(default_crf); compression_method_stack_->addWidget(crf_section_); bitrate_section_ = new H264BitRateSection(); @@ -107,7 +112,7 @@ void H264Section::AddOpts(EncodingParams *params) } } -H264CRFSection::H264CRFSection(QWidget *parent) : +H264CRFSection::H264CRFSection(int default_crf, QWidget *parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); @@ -116,15 +121,15 @@ H264CRFSection::H264CRFSection(QWidget *parent) : crf_slider_ = new QSlider(Qt::Horizontal); crf_slider_->setMinimum(kMinimumCRF); crf_slider_->setMaximum(kMaximumCRF); - crf_slider_->setValue(kDefaultCRF); + crf_slider_->setValue(default_crf); layout->addWidget(crf_slider_); IntegerSlider* crf_input = new IntegerSlider(); crf_input->setMaximumWidth(QtUtils::QFontMetricsWidth(crf_input->fontMetrics(), QStringLiteral("HHHH"))); crf_input->SetMinimum(kMinimumCRF); crf_input->SetMaximum(kMaximumCRF); - crf_input->SetValue(kDefaultCRF); - crf_input->SetDefaultValue(kDefaultCRF); + crf_input->SetValue(default_crf); + crf_input->SetDefaultValue(default_crf); layout->addWidget(crf_input); connect(crf_slider_, &QSlider::valueChanged, crf_input, &IntegerSlider::SetValue); @@ -211,4 +216,9 @@ int64_t H264FileSizeSection::GetFileSize() const return qRound64(file_size_->GetValue() * 1024.0 * 1024.0 * 8.0); } +H265Section::H265Section(QWidget *parent) : + H264Section(H264CRFSection::kDefaultH265CRF, parent) +{ +} + } diff --git a/app/dialog/export/codec/h264section.h b/app/dialog/export/codec/h264section.h index 5b71a431e..95f7fa84a 100644 --- a/app/dialog/export/codec/h264section.h +++ b/app/dialog/export/codec/h264section.h @@ -33,15 +33,15 @@ class H264CRFSection : public QWidget { Q_OBJECT public: - H264CRFSection(QWidget* parent = nullptr); + H264CRFSection(int default_crf, QWidget* parent = nullptr); int GetValue() const; + static const int kDefaultH264CRF = 23; + static const int kDefaultH265CRF = 28; + private: static const int kMinimumCRF = 0; - - static const int kDefaultCRF = 23; - static const int kMaximumCRF = 51; QSlider* crf_slider_; @@ -98,6 +98,7 @@ public: }; H264Section(QWidget* parent = nullptr); + H264Section(int default_crf, QWidget* parent); virtual void AddOpts(EncodingParams* params) override; @@ -112,6 +113,13 @@ private: }; +class H265Section : public H264Section +{ + Q_OBJECT +public: + H265Section(QWidget* parent = nullptr); +}; + } #endif // H264SECTION_H diff --git a/app/dialog/export/codec/imagesection.cpp b/app/dialog/export/codec/imagesection.cpp index 58d4cd423..caf7a0ef0 100644 --- a/app/dialog/export/codec/imagesection.cpp +++ b/app/dialog/export/codec/imagesection.cpp @@ -36,12 +36,22 @@ ImageSection::ImageSection(QWidget* parent) : layout->addWidget(new QLabel(tr("Image Sequence:")), row, 0); image_sequence_checkbox_ = new QCheckBox(); - layout->addWidget(new QCheckBox(), row, 1); + connect(image_sequence_checkbox_, &QCheckBox::toggled, this, &ImageSection::ImageSequenceCheckBoxToggled); + layout->addWidget(image_sequence_checkbox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Frame to Export:")), row, 0); + + frame_slider_ = new TimeSlider(); + frame_slider_->SetMinimum(0); + frame_slider_->SetValue(0); + layout->addWidget(frame_slider_, row, 1); } -QCheckBox *ImageSection::image_sequence_checkbox() const +void ImageSection::ImageSequenceCheckBoxToggled(bool e) { - return image_sequence_checkbox_; + frame_slider_->setEnabled(!e); } } diff --git a/app/dialog/export/codec/imagesection.h b/app/dialog/export/codec/imagesection.h index aa2e5976e..933678e17 100644 --- a/app/dialog/export/codec/imagesection.h +++ b/app/dialog/export/codec/imagesection.h @@ -24,6 +24,7 @@ #include #include "codecsection.h" +#include "widget/slider/timeslider.h" namespace olive { @@ -33,11 +34,34 @@ class ImageSection : public CodecSection public: ImageSection(QWidget* parent = nullptr); - QCheckBox* image_sequence_checkbox() const; + bool IsImageSequenceChecked() const + { + return image_sequence_checkbox_->isChecked(); + } + + void SetTimebase(const rational& r) + { + frame_slider_->SetTimebase(r); + } + + int64_t GetTimestamp() const + { + return frame_slider_->GetValue(); + } + + void SetTimestamp(int64_t t) + { + frame_slider_->SetValue(t); + } private: QCheckBox* image_sequence_checkbox_; + TimeSlider* frame_slider_; + +private slots: + void ImageSequenceCheckBoxToggled(bool e); + }; } diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 6ad525a16..0c3fceed8 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -30,6 +30,7 @@ #include #include +#include "common/digit.h" #include "common/qtutils.h" #include "core.h" #include "dialog/task/task.h" @@ -134,19 +135,19 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : row++; - QTabWidget* preferences_tabs = new QTabWidget(); + preferences_tabs_ = new QTabWidget(); QScrollArea* video_area = new QScrollArea(); color_manager_ = viewer_node_->project()->color_manager(); video_tab_ = new ExportVideoTab(color_manager_); video_area->setWidgetResizable(true); video_area->setWidget(video_tab_); - preferences_tabs->addTab(video_area, tr("Video")); + preferences_tabs_->addTab(video_area, tr("Video")); QScrollArea* audio_area = new QScrollArea(); audio_tab_ = new ExportAudioTab(); audio_area->setWidgetResizable(true); audio_area->setWidget(audio_tab_); - preferences_tabs->addTab(audio_area, tr("Audio")); - preferences_layout->addWidget(preferences_tabs, row, 0, 1, 4); + preferences_tabs_->addTab(audio_area, tr("Audio")); + preferences_layout->addWidget(preferences_tabs_, row, 0, 1, 4); row++; @@ -165,6 +166,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preview_layout->addWidget(new QLabel(tr("Preview"))); preview_viewer_ = new ViewerWidget(); preview_viewer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + connect(preview_viewer_, &ViewerWidget::TimeChanged, video_tab_, &ExportVideoTab::SetTimestamp); preview_layout->addWidget(preview_viewer_); splitter->addWidget(preview_area); @@ -176,17 +178,31 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : // Populate combobox formats for (int i=0; iaddItem(ExportFormat::GetName(static_cast(i))); + QString format_name = ExportFormat::GetName(static_cast(i)); + + bool inserted = false; + + for (int j=0; jcount(); j++) { + if (format_combobox_->itemText(j) > format_name) { + format_combobox_->insertItem(j, format_name, i); + inserted = true; + break; + } + } + + if (!inserted) { + format_combobox_->addItem(format_name, i); + } } // Set defaults previously_selected_format_ = ExportFormat::kFormatMPEG4; - format_combobox_->setCurrentIndex(ExportFormat::kFormatMPEG4); + SetCurrentFormat(ExportFormat::kFormatMPEG4); connect(format_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &ExportDialog::FormatChanged); - FormatChanged(ExportFormat::kFormatMPEG4); + FormatChanged(format_combobox_->currentIndex()); VideoParams vp = viewer_node_->GetVideoParams(); AudioParams ap = viewer_node_->GetAudioParams(); @@ -195,9 +211,9 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : video_tab_->width_slider()->SetDefaultValue(vp.width()); video_tab_->height_slider()->SetValue(vp.height()); video_tab_->height_slider()->SetDefaultValue(vp.height()); - video_tab_->frame_rate_combobox()->SetFrameRate(vp.frame_rate()); + video_tab_->SetSelectedFrameRate(vp.frame_rate()); video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); - video_tab_->pixel_format_field()->SetPixelFormat(static_cast(Config::Current()["OnlinePixelFormat"].toInt())); + video_tab_->pixel_format_field()->SetPixelFormat(static_cast(Config::Current()[QStringLiteral("OnlinePixelFormat")].toInt())); video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); @@ -228,6 +244,10 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : &ExportVideoTab::ColorSpaceChanged, preview_viewer_, static_cast(&ViewerWidget::SetColorTransform)); + connect(video_tab_, + &ExportVideoTab::ImageSequenceCheckBoxChanged, + this, + &ExportDialog::ImageSequenceCheckBoxChanged); // Set viewer to view the node preview_viewer_->ConnectViewerNode(viewer_node_); @@ -236,36 +256,35 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); } +ExportFormat::Format ExportDialog::GetSelectedFormat() const +{ + return static_cast(format_combobox_->currentData().toInt()); +} + +rational ExportDialog::GetSelectedTimebase() const +{ + return video_tab_->GetSelectedFrameRate().flipped(); +} + void ExportDialog::StartExport() { if (!video_enabled_->isChecked() && !audio_enabled_->isChecked()) { - QMessageBox b(this); - b.setIcon(QMessageBox::Critical); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Invalid parameters")); - b.setText(tr("Both video and audio are disabled. There's nothing to export.")); - b.addButton(QMessageBox::Ok); - b.exec(); + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid parameters"), + tr("Both video and audio are disabled. There's nothing to export.")); return; } // Validate if the entered filename contains the correct extension (the extension is necessary // for both FFmpeg and OIIO to determine the output format) - QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(static_cast(format_combobox_->currentIndex()))); + QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(GetSelectedFormat())); QString proposed_filename = filename_edit_->text().trimmed(); // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. if (!proposed_filename.endsWith(necessary_ext, Qt::CaseInsensitive)) { - QMessageBox b(this); - b.setIcon(QMessageBox::Warning); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Invalid filename")); - b.setText(tr("The filename must contain the extension \"%1\". Would you like to append it " - "automatically?").arg(necessary_ext)); - b.addButton(QMessageBox::Yes); - b.addButton(QMessageBox::No); - - if (b.exec() == QMessageBox::Yes) { + if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Invalid filename"), + tr("The filename must contain the extension \"%1\". Would you like to append it " + "automatically?").arg(necessary_ext), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { filename_edit_->setText(proposed_filename.append(necessary_ext)); } else { return; @@ -278,43 +297,50 @@ void ExportDialog::StartExport() // If the directory does not exist, try to create it if (!QDir(file_info.path()).mkpath(QStringLiteral("."))) { - QMessageBox b(this); - b.setIcon(QMessageBox::Critical); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Failed to create output directory")); - b.setText(tr("The intended output directory doesn't exist and Olive couldn't create it. " - "Please choose a different filename.")); - b.addButton(QMessageBox::Ok); - b.exec(); + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Failed to create output directory"), + tr("The intended output directory doesn't exist and Olive couldn't create it. " + "Please choose a different filename.")); return; } + // Validate if this is an image sequence and if the filename contains enough digits + if (video_tab_->IsImageSequenceSet()) { + // Ensure filename contains digits + if (!Encoder::FilenameContainsDigitPlaceholder(proposed_filename)) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + tr("Export is set to an image sequence, but the filename does not have a section for digits " + "(formatted as [#####] where the amount of # is the amount of digits).")); + return; + } + + int64_t frame_count = GetExportLengthInTimebaseUnits(); + int64_t needed_digit_count = GetDigitCount(frame_count); + int current_digit_count = Encoder::GetImageSequencePlaceholderDigitCount(proposed_filename); + if (current_digit_count < needed_digit_count) { + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + tr("Filename doesn't contain enough digits for the amount of frames " + "this export will need (need %1 for %n frame(s)).", nullptr, frame_count) + .arg(QString::number(needed_digit_count))); + return; + } + } + // Validate if the file exists and whether the user wishes to overwrite it if (file_info.exists()) { - QMessageBox b(this); - b.setIcon(QMessageBox::Warning); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Confirm Overwrite")); - b.setText(tr("The file \"%1\" already exists. Do you want to overwrite it?") - .arg(proposed_filename)); - b.addButton(QMessageBox::Yes); - b.addButton(QMessageBox::No); - - if (b.exec() == QMessageBox::No) { + if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Confirm Overwrite"), + tr("The file \"%1\" already exists. Do you want to overwrite it?") + .arg(proposed_filename), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { return; } } // Validate video resolution if (video_enabled_->isChecked() - && video_tab_->GetSelectedCodec() == ExportCodec::kCodecH264 + && (video_tab_->GetSelectedCodec() == ExportCodec::kCodecH264 || video_tab_->GetSelectedCodec() == ExportCodec::kCodecH265) && (video_tab_->width_slider()->GetValue()%2 != 0 || video_tab_->height_slider()->GetValue()%2 != 0)) { - QMessageBox b(this); - b.setIcon(QMessageBox::Critical); - b.setWindowModality(Qt::WindowModal); - b.setWindowTitle(tr("Invalid Parameters")); - b.setText(tr("Width and height must be multiples of 2.")); - b.exec(); + QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid Parameters"), + tr("Width and height must be multiples of 2.")); return; } @@ -336,6 +362,29 @@ void ExportDialog::ExportFinished() } } +void ExportDialog::ImageSequenceCheckBoxChanged(bool e) +{ + QFileInfo current_fileinfo(filename_edit_->text()); + + QString basename = current_fileinfo.completeBaseName(); + QString suffix = current_fileinfo.suffix(); + + if (e) { + if (!Encoder::FilenameContainsDigitPlaceholder(basename)) { + basename.append(QStringLiteral("_[#####]")); + } + } else { + basename = Encoder::FilenameRemoveDigitPlaceholder(basename); + } + + // Set filename + if (!suffix.isEmpty()) { + basename.append('.'); + basename.append(suffix); + } + filename_edit_->setText(current_fileinfo.dir().filePath(basename)); +} + void ExportDialog::closeEvent(QCloseEvent *e) { preview_viewer_->ConnectViewerNode(nullptr); @@ -345,7 +394,7 @@ void ExportDialog::closeEvent(QCloseEvent *e) void ExportDialog::BrowseFilename() { - ExportFormat::Format f = static_cast(format_combobox_->currentIndex()); + ExportFormat::Format f = GetSelectedFormat(); QString browsed_fn = QFileDialog::getSaveFileName(this, "", @@ -365,7 +414,7 @@ void ExportDialog::FormatChanged(int index) { QString current_filename = filename_edit_->text().trimmed(); QString previously_selected_ext = ExportFormat::GetExtension(previously_selected_format_); - ExportFormat::Format current_format = static_cast(index); + ExportFormat::Format current_format = static_cast(format_combobox_->itemData(index).toInt()); QString currently_selected_ext = ExportFormat::GetExtension(current_format); // If the previous extension was added, remove it @@ -381,15 +430,13 @@ void ExportDialog::FormatChanged(int index) previously_selected_format_ = current_format; // Update video and audio comboboxes - video_tab_->codec_combobox()->clear(); - foreach (ExportCodec::Codec vcodec, ExportFormat::GetVideoCodecs(current_format)) { - video_tab_->codec_combobox()->addItem(ExportCodec::GetCodecName(vcodec), vcodec); - } + bool has_video_codecs = video_tab_->SetFormat(current_format); + video_enabled_->setChecked(has_video_codecs); + video_enabled_->setEnabled(has_video_codecs); - audio_tab_->codec_combobox()->clear(); - foreach (ExportCodec::Codec acodec, ExportFormat::GetAudioCodecs(current_format)) { - audio_tab_->codec_combobox()->addItem(ExportCodec::GetCodecName(acodec), acodec); - } + bool has_audio_codecs = audio_tab_->SetFormat(current_format); + audio_enabled_->setChecked(has_audio_codecs); + audio_enabled_->setEnabled(has_audio_codecs); } void ExportDialog::ResolutionChanged() @@ -449,7 +496,7 @@ ExportParams ExportDialog::GenerateParams() const { VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()), - video_tab_->frame_rate_combobox()->GetFrameRate().flipped(), + GetSelectedTimebase(), video_tab_->pixel_format_field()->GetPixelFormat(), VideoParams::kInternalChannelCount, video_tab_->pixel_aspect_combobox()->GetPixelAspectRatio(), @@ -461,10 +508,15 @@ ExportParams ExportDialog::GenerateParams() const AudioParams::kInternalFormat); ExportParams params; + params.set_encoder(Encoder::GetTypeFromFormat(GetSelectedFormat())); params.SetFilename(filename_edit_->text().trimmed()); params.SetExportLength(viewer_node_->GetLength()); - if (range_combobox_->currentIndex() == kRangeInToOut) { + if (ExportCodec::IsCodecAStillImage(video_tab_->GetSelectedCodec()) && !video_tab_->IsImageSequenceSet()) { + // Exporting as image without exporting image sequence, only export one frame + rational export_time = Timecode::timestamp_to_time(video_tab_->GetStillImageTime(), GetSelectedTimebase()); + params.set_custom_range(TimeRange(export_time, export_time)); + } else if (range_combobox_->currentIndex() == kRangeInToOut) { // Assume if this combobox is enabled, workarea is enabled - a check that we make in this dialog's constructor params.set_custom_range(viewer_node_->GetTimelinePoints()->workarea()->range()); } @@ -479,11 +531,15 @@ ExportParams ExportDialog::GenerateParams() const params.set_video_threads(video_tab_->threads()); - video_tab_->GetCodecSection()->AddOpts(¶ms); + if (video_tab_->isVisible()) { + video_tab_->GetCodecSection()->AddOpts(¶ms); + } params.set_color_transform(video_tab_->CurrentOCIOColorSpace()); params.set_video_pix_fmt(video_tab_->pix_fmt()); + + params.set_video_is_image_sequence(video_tab_->IsImageSequenceSet()); } if (audio_enabled_->isChecked()) { @@ -496,6 +552,30 @@ ExportParams ExportDialog::GenerateParams() const return params; } +void ExportDialog::SetCurrentFormat(ExportFormat::Format format) +{ + for (int i=0; icount(); i++) { + if (format_combobox_->itemData(i).toInt() == format) { + format_combobox_->setCurrentIndex(i); + break; + } + } +} + +rational ExportDialog::GetExportLength() const +{ + if (range_combobox_->currentIndex() == kRangeInToOut) { + return viewer_node_->GetTimelinePoints()->workarea()->range().length(); + } else { + return viewer_node_->GetLength(); + } +} + +int64_t ExportDialog::GetExportLengthInTimebaseUnits() const +{ + return Timecode::time_to_timestamp(GetExportLength(), GetSelectedTimebase()); +} + void ExportDialog::UpdateViewerDimensions() { preview_viewer_->SetViewerResolution(static_cast(video_tab_->width_slider()->GetValue()), @@ -509,7 +589,7 @@ void ExportDialog::UpdateViewerDimensions() vp.height(), static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()) - ); + ); preview_viewer_->SetMatrix(transform); } diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 36ac8b110..99f6f88f6 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -42,6 +42,10 @@ class ExportDialog : public QDialog public: ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); + ExportFormat::Format GetSelectedFormat() const; + + rational GetSelectedTimebase() const; + protected: virtual void closeEvent(QCloseEvent *e) override; @@ -51,15 +55,22 @@ private: ExportParams GenerateParams() const; + void SetCurrentFormat(ExportFormat::Format format); + ViewerOutput* viewer_node_; ExportFormat::Format previously_selected_format_; + rational GetExportLength() const; + int64_t GetExportLengthInTimebaseUnits() const; + enum RangeSelection { kRangeEntireSequence, kRangeInToOut }; + QTabWidget* preferences_tabs_; + QComboBox* range_combobox_; QCheckBox* video_enabled_; @@ -92,6 +103,8 @@ private slots: void ExportFinished(); + void ImageSequenceCheckBoxChanged(bool e); + }; } diff --git a/app/dialog/export/exportaudiotab.cpp b/app/dialog/export/exportaudiotab.cpp index edb0e9e14..0db6dd0cd 100644 --- a/app/dialog/export/exportaudiotab.cpp +++ b/app/dialog/export/exportaudiotab.cpp @@ -75,4 +75,15 @@ ExportAudioTab::ExportAudioTab(QWidget* parent) : outer_layout->addStretch(); } +int ExportAudioTab::SetFormat(ExportFormat::Format format) +{ + QList acodecs = ExportFormat::GetAudioCodecs(format); + setEnabled(!acodecs.isEmpty()); + codec_combobox()->clear(); + foreach (ExportCodec::Codec acodec, acodecs) { + codec_combobox()->addItem(ExportCodec::GetCodecName(acodec), acodec); + } + return acodecs.size(); +} + } diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h index 888e8d803..ef5155018 100644 --- a/app/dialog/export/exportaudiotab.h +++ b/app/dialog/export/exportaudiotab.h @@ -25,6 +25,7 @@ #include #include "common/define.h" +#include "codec/exportformat.h" #include "widget/slider/integerslider.h" #include "widget/standardcombos/standardcombos.h" @@ -36,6 +37,8 @@ class ExportAudioTab : public QWidget public: ExportAudioTab(QWidget* parent = nullptr); + int SetFormat(ExportFormat::Format format); + QComboBox* codec_combobox() const { return codec_combobox_; diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 97b329551..3283bc957 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -49,6 +49,26 @@ ExportVideoTab::ExportVideoTab(ColorManager* color_manager, QWidget *parent) : outer_layout->addStretch(); } +int ExportVideoTab::SetFormat(ExportFormat::Format format) +{ + format_ = format; + + QList vcodecs = ExportFormat::GetVideoCodecs(format); + setEnabled(!vcodecs.isEmpty()); + codec_combobox()->clear(); + foreach (ExportCodec::Codec vcodec, vcodecs) { + codec_combobox()->addItem(ExportCodec::GetCodecName(vcodec), vcodec); + } + return vcodecs.size(); +} + +bool ExportVideoTab::IsImageSequenceSet() const +{ + ImageSection* img_section = dynamic_cast(codec_stack_->currentWidget()); + + return (img_section && img_section->IsImageSequenceChecked()); +} + QWidget* ExportVideoTab::SetupResolutionSection() { int row = 0; @@ -99,6 +119,7 @@ QWidget* ExportVideoTab::SetupResolutionSection() layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); frame_rate_combobox_ = new FrameRateComboBox(); + connect(frame_rate_combobox_, &FrameRateComboBox::FrameRateChanged, this, &ExportVideoTab::UpdateFrameRate); layout->addWidget(frame_rate_combobox_, row, 1); row++; @@ -161,6 +182,9 @@ QWidget *ExportVideoTab::SetupCodecSection() h264_section_ = new H264Section(); codec_stack_->addWidget(h264_section_); + h265_section_ = new H265Section(); + codec_stack_->addWidget(h265_section_); + row++; QPushButton* advanced_btn = new QPushButton(tr("Advanced")); @@ -178,7 +202,7 @@ void ExportVideoTab::MaintainAspectRatioChanged(bool val) void ExportVideoTab::OpenAdvancedDialog() { // Find export formats compatible with this encoder - QStringList pixel_formats = ExportCodec::GetPixelFormatsForCodec(GetSelectedCodec()); + QStringList pixel_formats = ExportFormat::GetPixelFormatsForCodec(format_, GetSelectedCodec()); ExportAdvancedVideoDialog d(pixel_formats, this); @@ -191,19 +215,51 @@ void ExportVideoTab::OpenAdvancedDialog() } } +void ExportVideoTab::UpdateFrameRate(rational r) +{ + // Convert frame rate to timebase + r.flip(); + + for (int i=0; icount(); i++) { + ImageSection* img = dynamic_cast(codec_stack_->widget(i)); + if (img) { + img->SetTimebase(r); + } + } +} + void ExportVideoTab::VideoCodecChanged() { ExportCodec::Codec codec = GetSelectedCodec(); if (codec == ExportCodec::kCodecH264) { - SetCodecSection(h264_section()); + SetCodecSection(h264_section_); + } else if (codec == ExportCodec::kCodecH265) { + SetCodecSection(h265_section_); } else if (ExportCodec::IsCodecAStillImage(codec)) { - SetCodecSection(image_section()); + SetCodecSection(image_section_); + } else { + SetCodecSection(nullptr); } // Set default pixel format - pix_fmt_ = ExportCodec::GetPixelFormatsForCodec(codec).first(); + QStringList pix_fmts = ExportFormat::GetPixelFormatsForCodec(format_, codec); + if (!pix_fmts.isEmpty()) { + pix_fmt_ = pix_fmts.first(); + } else { + pix_fmt_.clear(); + } qDebug() << "Set default pix fmt" << pix_fmt_; } +void ExportVideoTab::SetTimestamp(int64_t timestamp) +{ + for (int i=0; icount(); i++) { + ImageSection* img = dynamic_cast(codec_stack_->widget(i)); + if (img) { + img->SetTimestamp(timestamp); + } + } +} + } diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index 31770c901..45b2d50bb 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -41,6 +41,15 @@ class ExportVideoTab : public QWidget public: ExportVideoTab(ColorManager* color_manager, QWidget* parent = nullptr); + int SetFormat(ExportFormat::Format format); + + bool IsImageSequenceSet() const; + + int64_t GetStillImageTime() const + { + return image_section_->GetTimestamp(); + } + ExportCodec::Codec GetSelectedCodec() const { return static_cast(codec_combobox()->currentData().toInt()); @@ -71,9 +80,15 @@ public: return scaling_method_combobox_; } - FrameRateComboBox* frame_rate_combobox() const + rational GetSelectedFrameRate() const { - return frame_rate_combobox_; + return frame_rate_combobox_->GetFrameRate(); + } + + void SetSelectedFrameRate(const rational& fr) + { + frame_rate_combobox_->SetFrameRate(fr); + UpdateFrameRate(fr); } QString CurrentOCIOColorSpace() @@ -88,17 +103,12 @@ public: void SetCodecSection(CodecSection* section) { - codec_stack_->setCurrentWidget(section); - } - - ImageSection* image_section() const - { - return image_section_; - } - - H264Section* h264_section() const - { - return h264_section_; + if (section) { + codec_stack_->setVisible(true); + codec_stack_->setCurrentWidget(section); + } else { + codec_stack_->setVisible(false); + } } InterlacedComboBox* interlaced_combobox() const @@ -128,9 +138,13 @@ public: public slots: void VideoCodecChanged(); + void SetTimestamp(int64_t timestamp); + signals: void ColorSpaceChanged(const QString& colorspace); + void ImageSequenceCheckBoxChanged(bool e); + private: QWidget* SetupResolutionSection(); QWidget* SetupColorSection(); @@ -144,6 +158,7 @@ private: QStackedWidget* codec_stack_; ImageSection* image_section_; H264Section* h264_section_; + H264Section* h265_section_; ColorSpaceChooser* color_space_chooser_; @@ -160,11 +175,15 @@ private: QString pix_fmt_; + ExportFormat::Format format_; + private slots: void MaintainAspectRatioChanged(bool val); void OpenAdvancedDialog(); + void UpdateFrameRate(rational r); + }; } diff --git a/app/task/export/exportparams.cpp b/app/task/export/exportparams.cpp index 8249f6e3f..33d5acc45 100644 --- a/app/task/export/exportparams.cpp +++ b/app/task/export/exportparams.cpp @@ -28,12 +28,12 @@ ExportParams::ExportParams() : { } -const QString &ExportParams::encoder() const +const Encoder::Type &ExportParams::encoder() const { return encoder_id_; } -void ExportParams::set_encoder(const QString &id) +void ExportParams::set_encoder(const Encoder::Type &id) { encoder_id_ = id; } @@ -104,7 +104,7 @@ void ExportParams::Save(QXmlStreamWriter *writer) const { writer->writeStartElement(QStringLiteral("export")); - writer->writeTextElement(QStringLiteral("encoder"), encoder_id_); + writer->writeTextElement(QStringLiteral("encoder"), QString::number(encoder_id_)); writer->writeTextElement(QStringLiteral("vscale"), QString::number(video_scaling_method_)); diff --git a/app/task/export/exportparams.h b/app/task/export/exportparams.h index b9c266eda..6405e7740 100644 --- a/app/task/export/exportparams.h +++ b/app/task/export/exportparams.h @@ -39,8 +39,8 @@ public: ExportParams(); - const QString& encoder() const; - void set_encoder(const QString& id); + const Encoder::Type& encoder() const; + void set_encoder(const Encoder::Type& id); bool has_custom_range() const; const TimeRange& custom_range() const; @@ -59,7 +59,7 @@ public: virtual void Save(QXmlStreamWriter* writer) const override; private: - QString encoder_id_; + Encoder::Type encoder_id_; VideoScalingMethod video_scaling_method_;