diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index c9cd17350..2f67110c0 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -25,8 +25,7 @@ OLIVE_NAMESPACE_ENTER Encoder::Encoder(const EncodingParams ¶ms) : - params_(params), - open_(false) + params_(params) { } @@ -166,40 +165,4 @@ Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) return new FFmpegEncoder(params); } -bool Encoder::IsOpen() const -{ - return open_; -} - -void Encoder::Open() -{ - if (!open_) { - open_ = OpenInternal(); - } - - if (open_) { - emit OpenSucceeded(); - } else { - emit OpenFailed(); - } -} - -void Encoder::WriteFrame(FramePtr frame, rational time) -{ - if (open_) { - WriteInternal(frame, time); - } -} - -void Encoder::Close() -{ - if (open_) { - CloseInternal(); - - open_ = false; - } - - emit Closed(); -} - OLIVE_NAMESPACE_EXIT diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 05ce58760..adc20dd19 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -104,31 +104,17 @@ public: const EncodingParams& params() const; -public slots: - void Open(); - void WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time); - virtual void WriteAudio(OLIVE_NAMESPACE::AudioRenderingParams pcm_info, const QString& pcm_filename, OLIVE_NAMESPACE::TimeRange range) = 0; - void Close(); + virtual bool Open() = 0; -signals: - void OpenSucceeded(); - void OpenFailed(); + virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) = 0; + virtual void WriteAudio(OLIVE_NAMESPACE::AudioRenderingParams pcm_info, + const QString& pcm_filename, OLIVE_NAMESPACE::TimeRange range) = 0; - void Closed(); - - void AudioComplete(); - -protected: - virtual bool OpenInternal() = 0; - virtual void WriteInternal(FramePtr frame, rational time) = 0; - virtual void CloseInternal() = 0; - - bool IsOpen() const; + virtual void Close() = 0; private: EncodingParams params_; - bool open_; }; OLIVE_NAMESPACE_EXIT diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 86bbf9309..a95880372 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -35,10 +35,141 @@ FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) : video_scale_ctx_(nullptr), audio_stream_(nullptr), audio_codec_ctx_(nullptr), - audio_resample_ctx_(nullptr) + audio_resample_ctx_(nullptr), + open_(false) { } +bool FFmpegEncoder::Open() +{ + if (open_) { + return true; + } + + int error_code; + + // Convert QString to C string that FFmpeg expects + QByteArray filename_bytes = params().filename().toUtf8(); + const char* filename_c_str = filename_bytes.constData(); + + // Create output format context + error_code = avformat_alloc_output_context2(&fmt_ctx_, nullptr, nullptr, filename_c_str); + + // Check error code + if (error_code < 0) { + FFmpegError("Failed to allocate output context", error_code); + return false; + } + + // Initialize a video stream if it's enabled + if (params().video_enabled()) { + if (!InitializeStream(AVMEDIA_TYPE_VIDEO, &video_stream_, &video_codec_ctx_, params().video_codec())) { + return false; + } + + // This is the format we will expect frames received in Write() to be in + PixelFormat::Format native_pixel_fmt = params().video_params().format(); + + // This is the format we will need to convert the frame to for swscale to understand it + video_conversion_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(native_pixel_fmt); + + // This is the equivalent pixel format above as an AVPixelFormat that swscale can understand + AVPixelFormat src_pix_fmt = FFmpegCommon::GetFFmpegPixelFormat(video_conversion_fmt_); + + // This is the pixel format the encoder wants to encode to + AVPixelFormat encoder_pix_fmt = video_codec_ctx_->pix_fmt; + + // Set up a scaling context - if the native pixel format is not equal to the encoder's, we'll need to convert it + // before encoding. Even if we don't, this may be useful for converting between linesizes, etc. + video_scale_ctx_ = sws_getContext(params().video_params().width(), + params().video_params().height(), + src_pix_fmt, + params().video_params().width(), + params().video_params().height(), + encoder_pix_fmt, + 0, + nullptr, + nullptr, + nullptr); + } + + // Initialize an audio stream if it's enabled + if (params().audio_enabled() + && !InitializeStream(AVMEDIA_TYPE_AUDIO, &audio_stream_, &audio_codec_ctx_, params().audio_codec())) { + return false; + } + + av_dump_format(fmt_ctx_, 0, filename_c_str, 1); + + // Open output file for writing + error_code = avio_open(&fmt_ctx_->pb, filename_c_str, AVIO_FLAG_WRITE); + if (error_code < 0) { + FFmpegError("Failed to open IO context", error_code); + return false; + } + + // Write header + error_code = avformat_write_header(fmt_ctx_, nullptr); + if (error_code < 0) { + FFmpegError("Failed to write format header", error_code); + return false; + } + + open_ = true; + return true; +} + +bool FFmpegEncoder::WriteFrame(FramePtr frame, rational time) +{ + bool success = false; + + AVFrame* encoded_frame = av_frame_alloc(); + + int error_code; + const char* input_data; + int input_linesize; + + // Frame must be video + encoded_frame->width = frame->width(); + encoded_frame->height = frame->height(); + encoded_frame->format = video_codec_ctx_->pix_fmt; + + error_code = av_frame_get_buffer(encoded_frame, 0); + if (error_code < 0) { + FFmpegError("Failed to create AVFrame buffer", error_code); + goto fail; + } + + // We may need to convert this frame to a frame that swscale will understand + if (frame->format() != video_conversion_fmt_) { + frame = PixelFormat::ConvertPixelFormat(frame, video_conversion_fmt_); + } + + // Use swscale context to convert formats/linesizes + input_data = frame->const_data(); + input_linesize = frame->linesize_bytes(); + error_code = sws_scale(video_scale_ctx_, + reinterpret_cast(&input_data), + &input_linesize, + 0, + frame->height(), + encoded_frame->data, + encoded_frame->linesize); + if (error_code < 0) { + FFmpegError("Failed to scale frame", error_code); + goto fail; + } + + encoded_frame->pts = qRound64(time.toDouble() / av_q2d(video_codec_ctx_->time_base)); + + success = WriteAVFrame(encoded_frame, video_codec_ctx_, video_stream_); + +fail: + av_frame_free(&encoded_frame); + + return success; +} + void FFmpegEncoder::WriteAudio(AudioRenderingParams pcm_info, const QString &pcm_filename, TimeRange range) { QFile pcm(pcm_filename); @@ -150,140 +281,19 @@ void FFmpegEncoder::WriteAudio(AudioRenderingParams pcm_info, const QString &pcm pcm.close(); } - - emit AudioComplete(); } -bool FFmpegEncoder::OpenInternal() +void FFmpegEncoder::Close() { - int error_code; - - // Convert QString to C string that FFmpeg expects - QByteArray filename_bytes = params().filename().toUtf8(); - const char* filename_c_str = filename_bytes.constData(); - - // Create output format context - error_code = avformat_alloc_output_context2(&fmt_ctx_, nullptr, nullptr, filename_c_str); - - // Check error code - if (error_code < 0) { - FFmpegError("Failed to allocate output context", error_code); - return false; - } - - // Initialize a video stream if it's enabled - if (params().video_enabled()) { - if (!InitializeStream(AVMEDIA_TYPE_VIDEO, &video_stream_, &video_codec_ctx_, params().video_codec())) { - return false; - } - - // This is the format we will expect frames received in Write() to be in - PixelFormat::Format native_pixel_fmt = params().video_params().format(); - - // This is the format we will need to convert the frame to for swscale to understand it - video_conversion_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(native_pixel_fmt); - - // This is the equivalent pixel format above as an AVPixelFormat that swscale can understand - AVPixelFormat src_pix_fmt = FFmpegCommon::GetFFmpegPixelFormat(video_conversion_fmt_); - - // This is the pixel format the encoder wants to encode to - AVPixelFormat encoder_pix_fmt = video_codec_ctx_->pix_fmt; - - // Set up a scaling context - if the native pixel format is not equal to the encoder's, we'll need to convert it - // before encoding. Even if we don't, this may be useful for converting between linesizes, etc. - video_scale_ctx_ = sws_getContext(params().video_params().width(), - params().video_params().height(), - src_pix_fmt, - params().video_params().width(), - params().video_params().height(), - encoder_pix_fmt, - 0, - nullptr, - nullptr, - nullptr); - } - - // Initialize an audio stream if it's enabled - if (params().audio_enabled() - && !InitializeStream(AVMEDIA_TYPE_AUDIO, &audio_stream_, &audio_codec_ctx_, params().audio_codec())) { - return false; - } - - av_dump_format(fmt_ctx_, 0, filename_c_str, 1); - - // Open output file for writing - error_code = avio_open(&fmt_ctx_->pb, filename_c_str, AVIO_FLAG_WRITE); - if (error_code < 0) { - FFmpegError("Failed to open IO context", error_code); - return false; - } - - // Write header - error_code = avformat_write_header(fmt_ctx_, nullptr); - if (error_code < 0) { - FFmpegError("Failed to write format header", error_code); - return false; - } - - return true; -} - -void FFmpegEncoder::WriteInternal(FramePtr frame, rational time) -{ - AVFrame* encoded_frame = av_frame_alloc(); - - int error_code; - const char* input_data; - int input_linesize; - - // Frame must be video - encoded_frame->width = frame->width(); - encoded_frame->height = frame->height(); - encoded_frame->format = video_codec_ctx_->pix_fmt; - - error_code = av_frame_get_buffer(encoded_frame, 0); - if (error_code < 0) { - FFmpegError("Failed to create AVFrame buffer", error_code); - goto fail; - } - - // We may need to convert this frame to a frame that swscale will understand - if (frame->format() != video_conversion_fmt_) { - frame = PixelFormat::ConvertPixelFormat(frame, video_conversion_fmt_); - } - - // Use swscale context to convert formats/linesizes - input_data = frame->const_data(); - input_linesize = frame->linesize_bytes(); - error_code = sws_scale(video_scale_ctx_, - reinterpret_cast(&input_data), - &input_linesize, - 0, - frame->height(), - encoded_frame->data, - encoded_frame->linesize); - if (error_code < 0) { - FFmpegError("Failed to scale frame", error_code); - goto fail; - } - - encoded_frame->pts = qRound64(time.toDouble() / av_q2d(video_codec_ctx_->time_base)); - - WriteAVFrame(encoded_frame, video_codec_ctx_, video_stream_); - -fail: - av_frame_free(&encoded_frame); -} - -void FFmpegEncoder::CloseInternal() -{ - if (IsOpen()) { + if (open_) { // Flush encoders FlushEncoders(); // We've written a header, so we'll write a trailer av_write_trailer(fmt_ctx_); avio_closep(&fmt_ctx_->pb); + + open_ = false; } if (video_scale_ctx_) { diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 8130db773..1a6e8261c 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -38,13 +38,14 @@ class FFmpegEncoder : public Encoder public: FFmpegEncoder(const EncodingParams ¶ms); -public slots: - virtual void WriteAudio(OLIVE_NAMESPACE::AudioRenderingParams pcm_info, const QString& pcm_filename, OLIVE_NAMESPACE::TimeRange range) override; + virtual bool Open() override; -protected: - virtual bool OpenInternal() override; - virtual void WriteInternal(FramePtr frame, rational time) override; - virtual void CloseInternal() override; + virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) override; + + virtual void WriteAudio(OLIVE_NAMESPACE::AudioRenderingParams pcm_info, + const QString& pcm_filename, OLIVE_NAMESPACE::TimeRange range) override; + + virtual void Close() override; private: /** @@ -86,6 +87,8 @@ private: AVCodecContext* audio_codec_ctx_; SwrContext* audio_resample_ctx_; + bool open_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index b13a90781..abffce1b3 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -67,7 +67,10 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : file_browse_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); file_browse_btn->setIcon(icon::Folder); file_browse_btn->setToolTip(tr("Browse for exported file filename")); - connect(file_browse_btn, SIGNAL(clicked(bool)), this, SLOT(BrowseFilename())); + connect(file_browse_btn, + &QPushButton::clicked, + this, + &ExportDialog::BrowseFilename); preferences_layout->addWidget(file_browse_btn, row, 3); row++; @@ -102,7 +105,10 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preferences_layout->addWidget(new QLabel(tr("Format:")), row, 0); format_combobox_ = new QComboBox(); - connect(format_combobox_, SIGNAL(currentIndexChanged(int)), this, SLOT(FormatChanged(int))); + connect(format_combobox_, + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportDialog::FormatChanged); preferences_layout->addWidget(format_combobox_, row, 1, 1, 3); row++; @@ -141,7 +147,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : buttons_->setCenterButtons(true); buttons_->addButton(tr("Export"), QDialogButtonBox::AcceptRole); buttons_->addButton(QDialogButtonBox::Cancel); - connect(buttons_, &QDialogButtonBox::accepted, this, &ExportDialog::accept); + connect(buttons_, &QDialogButtonBox::accepted, this, &ExportDialog::StartExport); connect(buttons_, &QDialogButtonBox::rejected, this, &ExportDialog::reject); preferences_layout->addWidget(buttons_, row, 0, 1, 4); @@ -178,11 +184,31 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : video_aspect_ratio_ = static_cast(viewer_node_->video_params().width()) / static_cast(viewer_node_->video_params().height()); - connect(video_tab_->width_slider(), SIGNAL(ValueChanged(int64_t)), this, SLOT(ResolutionChanged())); - connect(video_tab_->height_slider(), SIGNAL(ValueChanged(int64_t)), this, SLOT(ResolutionChanged())); - connect(video_tab_->scaling_method_combobox(), SIGNAL(currentIndexChanged(int)), this, SLOT(UpdateViewerDimensions())); - connect(video_tab_->maintain_aspect_checkbox(), SIGNAL(toggled(bool)), this, SLOT(ResolutionChanged())); - connect(video_tab_->codec_combobox(), SIGNAL(currentIndexChanged(int)), this, SLOT(VideoCodecChanged())); + connect(video_tab_->width_slider(), + &IntegerSlider::ValueChanged, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_->height_slider(), + &IntegerSlider::ValueChanged, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_->scaling_method_combobox(), + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportDialog::UpdateViewerDimensions); + + connect(video_tab_->maintain_aspect_checkbox(), + &QCheckBox::toggled, + this, + &ExportDialog::ResolutionChanged); + + connect(video_tab_->codec_combobox(), + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportDialog::VideoCodecChanged); + connect(video_tab_, &ExportVideoTab::ColorSpaceChanged, preview_viewer_, @@ -194,7 +220,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); } -void ExportDialog::accept() +void ExportDialog::StartExport() { if (!video_enabled_->isChecked() && !audio_enabled_->isChecked()) { QMessageBox b(this); @@ -278,6 +304,7 @@ void ExportDialog::accept() ExportTask* task = new ExportTask(viewer_node_, color_manager_, GenerateParams()); TaskDialog* td = new TaskDialog(task, tr("Export"), this); + connect(td, &TaskDialog::TaskSucceeded, this, &QDialog::accept); td->open(); } diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 732410bcd..3bc1a0b64 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -42,9 +42,6 @@ class ExportDialog : public QDialog public: ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); -public slots: - virtual void accept() override; - protected: virtual void closeEvent(QCloseEvent *e) override; @@ -123,6 +120,8 @@ private slots: void UpdateViewerDimensions(); + void StartExport(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index cd961c353..e2bca4f68 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -474,8 +474,8 @@ void OpenGLProxy::TextureToBuffer(const QVariant& tex_in, 0, frame->width(), frame->height(), - OpenGLRenderFunctions::GetPixelFormat(texture->texture()->format()), - OpenGLRenderFunctions::GetPixelType(texture->texture()->format()), + OpenGLRenderFunctions::GetPixelFormat(frame->format()), + OpenGLRenderFunctions::GetPixelType(frame->format()), frame->data()); functions_->glPixelStorei(GL_PACK_ROW_LENGTH, 0); diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index cac1c18a7..1ccf6efbe 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -34,7 +34,7 @@ OLIVE_NAMESPACE_ENTER RenderBackend::RenderBackend(QObject *parent) : QObject(parent), viewer_node_(nullptr), - audio_enabled_(true), + audio_mode_(kAudioPreview), divider_(1), render_mode_(RenderMode::kOnline), pix_fmt_(PixelFormat::PIX_FMT_RGBA32F), @@ -91,7 +91,7 @@ void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) this, &RenderBackend::NodeGraphChanged); - if (audio_enabled_) { + if (audio_mode_ == kAudioPreview) { // Listen for audio invalidation signals connect(viewer_node_->audio_playback_cache(), &AudioPlaybackCache::Invalidated, @@ -161,9 +161,9 @@ void RenderBackend::SetVideoDownloadMatrix(const QMatrix4x4 &mat) video_download_matrix_ = mat; } -void RenderBackend::SetAudioEnabled(bool e) +void RenderBackend::SetAudioMode(AudioMode e) { - audio_enabled_ = e; + audio_mode_ = e; } void RenderBackend::WorkerStartedRenderingAudio(const TimeRange &r) @@ -391,7 +391,8 @@ void RenderBackend::AudioInvalidated(const TimeRange& r) watcher->setFuture(QtConcurrent::run(&audio_pool_.threads, GetInstanceFromPool(audio_pool_), &RenderWorker::RenderAudio, - this_range)); + this_range, + audio_mode_ == kAudioPreview)); } } diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index 0e6b79bc9..867666681 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -64,7 +64,13 @@ public: void SetVideoDownloadMatrix(const QMatrix4x4& mat); - void SetAudioEnabled(bool e); + enum AudioMode { + kAudioDisabled, + kAudioPreview, + kAudioRender + }; + + void SetAudioMode(AudioMode e); void WorkerStartedRenderingAudio(const TimeRange& r); @@ -107,7 +113,7 @@ private: QMutex queued_audio_lock_; TimeRangeList queued_audio_; - bool audio_enabled_; + AudioMode audio_mode_; // VIDEO MEMBERS int divider_; diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 11ace410d..9310875fd 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -99,7 +99,7 @@ FramePtr RenderWorker::RenderFrame(const rational &time, bool block_for_update) return frame; } -SampleBufferPtr RenderWorker::RenderAudio(const TimeRange &range) +SampleBufferPtr RenderWorker::RenderAudio(const TimeRange &range, bool block_for_update) { if (!viewer_) { return nullptr; @@ -109,7 +109,7 @@ SampleBufferPtr RenderWorker::RenderAudio(const TimeRange &range) parent_->WorkerStartedRenderingAudio(range); - UpdateData(true); + UpdateData(block_for_update); audio_render_time_ = range; diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index bd70c5678..c5c835c68 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -100,7 +100,7 @@ public: */ FramePtr RenderFrame(const rational& time, bool block_for_update); - SampleBufferPtr RenderAudio(const TimeRange& range); + SampleBufferPtr RenderAudio(const TimeRange& range, bool block_for_update); protected: virtual void TextureToFrame(const QVariant& texture, FramePtr frame, const QMatrix4x4 &mat) const = 0; diff --git a/app/task/cache/cache.cpp b/app/task/cache/cache.cpp index f06cb07df..35bd1bd79 100644 --- a/app/task/cache/cache.cpp +++ b/app/task/cache/cache.cpp @@ -21,6 +21,7 @@ #include "cache.h" #include +#include #include "project/item/sequence/sequence.h" @@ -48,9 +49,21 @@ bool CacheTask::Run() } } - Render(range_to_cache, 2); + Render(range_to_cache, RenderMode::kOffline, QMatrix4x4(), false, divider_); return true; } +QFuture CacheTask::DownloadFrame(FramePtr frame, const QByteArray &hash) +{ + return QtConcurrent::run(FrameHashCache::SaveCacheFrame, hash, frame); +} + +void CacheTask::FrameDownloaded(const QByteArray &hash, const QLinkedList ×) +{ + foreach (const rational& t, times) { + viewer()->video_frame_cache()->SetHash(t, hash); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/cache.h b/app/task/cache/cache.h index 74ac17d14..9efbd13dd 100644 --- a/app/task/cache/cache.h +++ b/app/task/cache/cache.h @@ -36,6 +36,11 @@ public: public slots: virtual bool Run() override; +protected: + virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; + + virtual void FrameDownloaded(const QByteArray& hash, const QLinkedList& times) override; + private: bool in_out_only_; diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 40ed46537..e57732170 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -20,6 +20,9 @@ #include "export.h" +#include "common/timecodefunctions.h" +#include "render/colormanager.h" + OLIVE_NAMESPACE_ENTER ExportTask::ExportTask(ViewerOutput* viewer_node, @@ -29,11 +32,125 @@ ExportTask::ExportTask(ViewerOutput* viewer_node, color_manager_(color_manager), params_(params) { + SetTitle(tr("Exporting \"%1\"").arg(viewer_node->media_name())); } bool ExportTask::Run() { - return true; + TimeRange range; + + encoder_ = Encoder::CreateFromID(params_.encoder(), params_); + if (!encoder_) { + SetError(tr("Failed to create encoder")); + return false; + } + + if (!encoder_->Open()) { + SetError(tr("Failed to open file")); + encoder_->deleteLater(); + return false; + } + + if (params_.has_custom_range()) { + // Render custom range only + range = params_.custom_range(); + } else { + // Render entire sequence + range = TimeRange(0, viewer()->GetLength()); + } + + frame_time_ = Timecode::time_to_timestamp(range.in(), viewer()->video_params().time_base()); + + QMatrix4x4 mat; + + if (params_.video_enabled()) { + + // If a transformation matrix is applied to this video, create it here + if (params_.video_scaling_method() != ExportParams::kStretch) { + mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), + viewer()->video_params().width(), + viewer()->video_params().height(), + params_.video_params().width(), + params_.video_params().height()); + } + + // Create color processor + color_processor_ = ColorProcessor::Create(color_manager_, + color_manager_->GetReferenceColorSpace(), + params_.color_transform()); + } + + TimeRangeList ranges; + ranges.InsertTimeRange(range); + Render(ranges, RenderMode::kOnline, mat, true, 1); + + bool success = true; + + foreach (QFuture f, write_frame_futures_) { + f.waitForFinished(); + + if (!f.result()) { + SetError(tr("Failed to write AVFrame")); + success = false; + } + } + + encoder_->Close(); + + encoder_->deleteLater(); + + return success; +} + +void FrameColorConvert(ColorProcessorPtr processor, FramePtr frame) +{ + qDebug() << "Converting" << frame->timestamp() << "from" << frame->format(); + + // OCIO conversion requires a frame in 32F format + if (frame->format() != PixelFormat::PIX_FMT_RGBA32F) { + frame = PixelFormat::ConvertPixelFormat(frame, PixelFormat::PIX_FMT_RGBA32F); + } + + // Color conversion must be done with unassociated alpha, and the pipeline is always associated + ColorManager::DisassociateAlpha(frame); + + // Convert color space + processor->ConvertFrame(frame); + + // Re-associate alpha + ColorManager::ReassociateAlpha(frame); +} + +QFuture ExportTask::DownloadFrame(FramePtr frame, const QByteArray &hash) +{ + rendered_frame_.insert(hash, frame); + + return QtConcurrent::run(FrameColorConvert, color_processor_, frame); +} + +void ExportTask::FrameDownloaded(const QByteArray &hash, const QLinkedList ×) +{ + FramePtr f = rendered_frame_.value(hash); + + foreach (const rational& t, times) { + time_map_.insert(t, f); + } + + forever { + rational real_time = Timecode::timestamp_to_time(frame_time_, + viewer()->video_params().time_base()); + + if (!time_map_.contains(real_time)) { + break; + } + + // Unfortunately this can't be done in another thread since the frames need to be sent + // one after the other chronologically. + encoder_->WriteFrame(time_map_.value(real_time), real_time); + + frame_time_++; + + } } OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/export.h b/app/task/export/export.h index 9c8cf6182..47e77a667 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -38,11 +38,28 @@ public: public slots: virtual bool Run() override; +protected: + virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; + + virtual void FrameDownloaded(const QByteArray& hash, const QLinkedList& times) override; + private: + QHash rendered_frame_; + + QHash time_map_; + + QList< QFuture > write_frame_futures_; + ColorManager* color_manager_; ExportParams params_; + Encoder* encoder_; + + ColorProcessorPtr color_processor_; + + int64_t frame_time_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index effebbe4b..6e8d1aaf2 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -36,6 +36,11 @@ struct TimeHashFuturePair { QFuture hash_future; }; +struct HashTimePair { + rational time; + QByteArray hash; +}; + struct HashFrameFuturePair { QByteArray hash; QFuture frame_future; @@ -46,24 +51,23 @@ struct HashDownloadFuturePair { QFuture download_future; }; -struct HashTimePair { - rational time; - QByteArray hash; -}; - -void RenderTask::Render(TimeRangeList range_to_cache, int divider) +void RenderTask::Render(TimeRangeList range_to_cache, + RenderMode::Mode mode, + const QMatrix4x4& mat, + bool audio_enabled, + int divider) { OpenGLBackend backend; - RenderMode::Mode mode = RenderMode::kOffline; PixelFormat::Format format = PixelFormat::instance()->GetConfiguredFormatForMode(mode); - backend.SetAudioEnabled(false); + backend.SetAudioMode(audio_enabled ? RenderBackend::kAudioRender : RenderBackend::kAudioDisabled); backend.SetViewerNode(viewer_); backend.SetPixelFormat(format); backend.SetMode(mode); backend.SetDivider(divider); backend.SetSampleFormat(SampleFormat::kInternalFormat); + backend.SetVideoDownloadMatrix(mat); // Get hashes for each frame QLinkedList hash_list; @@ -126,58 +130,52 @@ void RenderTask::Render(TimeRangeList range_to_cache, int divider) } } + // Start downloading frames that have finished OIIO::TypeDesc output_desc = PixelFormat::GetOIIOTypeDesc(format); - OIIO::ImageSpec output_spec(viewer_->video_params().width() / divider, - viewer_->video_params().height() / divider, + OIIO::ImageSpec output_spec(viewer()->video_params().width() / divider, + viewer()->video_params().height() / divider, PixelFormat::ChannelCount(format), output_desc); - // Start downloading frames that have finished - { - int counter = 0; - int nb_frames = render_lookup_table.size(); + int counter = 0; + int nb_frames = render_lookup_table.size(); - QLinkedList download_futures; + QLinkedList download_futures; - // Iterators - QLinkedList::iterator i; - QLinkedList::iterator j; + // Iterators + QLinkedList::iterator i; + QLinkedList::iterator j; - while (!render_lookup_table.isEmpty() || !download_futures.isEmpty()) { - i = render_lookup_table.begin(); + while (!render_lookup_table.isEmpty() || !download_futures.isEmpty()) { + i = render_lookup_table.begin(); - while (i != render_lookup_table.end()) { - if (i->frame_future.isFinished()) { - FramePtr f = i->frame_future.result(); + while (i != render_lookup_table.end()) { + if (i->frame_future.isFinished()) { + FramePtr f = i->frame_future.result(); - // Start multithreaded download here - download_futures.append({i->hash, - QtConcurrent::run(FrameHashCache::SaveCacheFrame, i->hash, f)}); + // Start multithreaded download here + download_futures.append({i->hash, DownloadFrame(f, i->hash)}); - i = render_lookup_table.erase(i); - } else { - i++; - } + i = render_lookup_table.erase(i); + } else { + i++; } + } - j = download_futures.begin(); + j = download_futures.begin(); - while (j != download_futures.end()) { - if (j->download_future.isFinished()) { - // Place it in the cache - const QLinkedList& times_with_hash = times_to_render.value(j->hash); - foreach (const rational& t, times_with_hash) { - viewer_->video_frame_cache()->SetHash(t, j->hash); - } + while (j != download_futures.end()) { + if (j->download_future.isFinished()) { + // Place it in the cache + FrameDownloaded(j->hash, times_to_render.value(j->hash)); - // Signal process - counter++; - emit ProgressChanged(static_cast(counter) / static_cast(nb_frames)); + // Signal process + counter++; + emit ProgressChanged(static_cast(counter) / static_cast(nb_frames)); - j = download_futures.erase(j); - } else { - j++; - } + j = download_futures.erase(j); + } else { + j++; } } } diff --git a/app/task/render/render.h b/app/task/render/render.h index 8931c5245..d3a565ac6 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -21,6 +21,8 @@ #ifndef RENDERTASK_H #define RENDERTASK_H +#include + #include "node/output/viewer/viewer.h" #include "task/task.h" @@ -32,13 +34,17 @@ public: RenderTask(ViewerOutput* viewer); protected: - void Render(TimeRangeList range_to_cache, int divider = 1); + void Render(TimeRangeList range_to_cache, RenderMode::Mode mode, const QMatrix4x4 &mat, bool audio_enabled, int divider); ViewerOutput* viewer() const { return viewer_; } + virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) = 0; + + virtual void FrameDownloaded(const QByteArray& hash, const QLinkedList& times) = 0; + private: ViewerOutput* viewer_; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index bdbed11e1..114e3c93e 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -979,7 +979,7 @@ void ViewerWidget::PlaybackTimerUpdate() tripped_time = max_time; } - if (Config::Current()["Loop"].toBool()) { + if (Config::Current()[QStringLiteral("Loop")].toBool()) { // If we're looping, jump to the other side of the workarea and continue int64_t opposing_time = (tripped_time == min_time) ? max_time : min_time; @@ -1044,7 +1044,7 @@ void ViewerWidget::SetZoomFromMenu(QAction *action) void ViewerWidget::ViewerInvalidatedRange(const TimeRange &range) { - if (GetTime() >= range.in() && GetTime() < range.out()) { + if (GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) { ForceUpdate(); } }