/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
***/
#include "exporter.h"
#include "render/backend/audio/audiobackend.h"
#include "render/backend/opengl/openglbackend.h"
#include "render/colormanager.h"
#include "render/pixelformat.h"
OLIVE_NAMESPACE_ENTER
Exporter::Exporter(ViewerOutput *viewer_node,
ColorManager *color_manager,
const ExportParams& params,
QObject* parent) :
QObject(parent),
viewer_node_(viewer_node),
params_(params),
video_backend_(nullptr),
audio_backend_(nullptr),
export_status_(false),
export_msg_(tr("Export hasn't started yet"))
{
encoder_ = Encoder::CreateFromID(params_.encoder(), params_);
video_done_ = !params_.video_enabled();
audio_done_ = !params_.audio_enabled();
debug_timer_.setInterval(5000);
connect(&debug_timer_, &QTimer::timeout, this, &Exporter::DebugTimerMessage);
connect(this, &Exporter::ExportEnded, this, &Exporter::deleteLater);
if (params_.has_custom_range()) {
export_range_ = params_.custom_range();
} else {
export_range_ = TimeRange(0, viewer_node_->Length());
}
if (params_.video_enabled()) {
// If a transformation matrix is applied to this video, create it here
if (params_.video_scaling_method() != ExportParams::kStretch) {
transform_ = GenerateMatrix(params_.video_scaling_method(),
viewer_node_->video_params().width(),
viewer_node_->video_params().height(),
params_.video_params().width(),
params_.video_params().height());
}
// Create color processor
color_processor_ = ColorProcessor::Create(color_manager,
color_manager->GetReferenceColorSpace(),
params.ocio_display(),
params.ocio_view(),
params.ocio_look());
}
}
bool Exporter::GetExportStatus() const
{
return export_status_;
}
const QString &Exporter::GetExportError() const
{
return export_msg_;
}
void Exporter::Cancel()
{
if (video_backend_) {
video_backend_->CancelQueue();
video_backend_->deleteLater();
video_backend_ = nullptr;
}
if (audio_backend_) {
audio_backend_->CancelQueue();
audio_backend_->deleteLater();
audio_backend_ = nullptr;
}
SetExportMessage(tr("User cancelled export"));
ExportStopped();
}
void Exporter::StartExporting()
{
// Default to error state until ExportEnd is called
export_status_ = false;
// Create renderers
if (!video_done_) {
video_backend_ = new OpenGLBackend();
video_backend_->SetLimitCaching(false);
video_backend_->SetViewerNode(viewer_node_);
video_backend_->SetParameters(VideoRenderingParams(viewer_node_->video_params().width(),
viewer_node_->video_params().height(),
params_.video_params().time_base(),
params_.video_params().format(),
params_.video_params().mode()));
waiting_for_frame_ = 0;
}
if (!audio_done_) {
audio_backend_ = new AudioBackend();
audio_backend_->SetViewerNode(viewer_node_);
audio_backend_->SetParameters(params_.audio_params());
}
// Open encoder and wait for result
connect(encoder_, &Encoder::OpenSucceeded, this, &Exporter::EncoderOpenedSuccessfully, Qt::QueuedConnection);
connect(encoder_, &Encoder::OpenFailed, this, &Exporter::EncoderOpenFailed, Qt::QueuedConnection);
connect(encoder_, &Encoder::AudioComplete, this, &Exporter::AudioEncodeComplete, Qt::QueuedConnection);
QMetaObject::invokeMethod(encoder_,
"Open",
Qt::QueuedConnection);
}
void Exporter::SetExportMessage(const QString &s)
{
export_msg_ = s;
}
void Exporter::ExportSucceeded()
{
if (!audio_done_ || !video_done_) {
return;
}
if (video_backend_) {
video_backend_->deleteLater();
video_backend_ = nullptr;
}
export_status_ = true;
connect(encoder_, &Encoder::Closed, this, &Exporter::EncoderClosed);
QMetaObject::invokeMethod(encoder_,
"Close",
Qt::QueuedConnection);
}
void Exporter::ExportStopped()
{
emit ExportEnded();
encoder_->deleteLater();
}
void Exporter::EncodeFrame()
{
while (cached_frames_.contains(waiting_for_frame_)) {
FramePtr frame = cached_frames_.take(waiting_for_frame_);
// 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
color_processor_->ConvertFrame(frame);
// Set frame timestamp
frame->set_timestamp(waiting_for_frame_);
// Encode (may require re-associating alpha?)
QMetaObject::invokeMethod(encoder_,
"WriteFrame",
Qt::QueuedConnection,
OLIVE_NS_ARG(FramePtr, frame));
waiting_for_frame_ += params_.video_params().time_base();
// Calculate progress
emit ProgressChanged(waiting_for_frame_.toDouble() / viewer_node_->Length().toDouble());
}
if (waiting_for_frame_ >= viewer_node_->Length()) {
video_done_ = true;
debug_timer_.stop();
ExportSucceeded();
}
}
QMatrix4x4 Exporter::GenerateMatrix(ExportParams::VideoScalingMethod method, int source_width, int source_height, int dest_width, int dest_height)
{
QMatrix4x4 preview_matrix;
if (method == ExportParams::kStretch) {
return preview_matrix;
}
float export_ar = static_cast(dest_width) / static_cast(dest_height);
float source_ar = static_cast(source_width) / static_cast(source_height);
if (qFuzzyCompare(export_ar, source_ar)) {
return preview_matrix;
}
if ((export_ar > source_ar) == (method == ExportParams::kFit)) {
preview_matrix.scale(source_ar / export_ar, 1.0F);
} else {
preview_matrix.scale(1.0F, export_ar / source_ar);
}
return preview_matrix;
}
void Exporter::FrameRendered(const rational &time, FramePtr value)
{
debug_timer_.stop();
const QMap& time_hash_map = video_backend_->frame_cache()->time_hash_map();
QByteArray this_hash = time_hash_map.value(time);
qDebug() << "Received" << this_hash.toHex();
QList matching_times = time_hash_map.keys(this_hash);
foreach (const rational& t, matching_times) {
qDebug() << " Matches" << t.toDouble();
cached_frames_.insert(t, value);
}
qDebug() << " Waiting for" << waiting_for_frame_.toDouble();
debug_timer_.start();
EncodeFrame();
}
void Exporter::AudioRendered()
{
// Retrieve the audio filename
QString cache_fn = audio_backend_->CachePathName();
QMetaObject::invokeMethod(encoder_,
"WriteAudio",
Qt::QueuedConnection,
OLIVE_NS_ARG(AudioRenderingParams, audio_backend_->params()),
Q_ARG(const QString&, cache_fn),
OLIVE_NS_ARG(TimeRange, export_range_));
// We don't need the audio backend anymore
audio_backend_->deleteLater();
audio_backend_ = nullptr;
}
void Exporter::AudioEncodeComplete()
{
audio_done_ = true;
ExportSucceeded();
}
void Exporter::EncoderOpenedSuccessfully()
{
// Invalidate caches
if (!video_done_) {
// First we generate the hashes so we know exactly how many frames we need
video_backend_->SetOperatingMode(VideoRenderWorker::kHashOnly);
connect(video_backend_, &VideoRenderBackend::QueueComplete, this, &Exporter::VideoHashesComplete);
video_backend_->InvalidateCache(export_range_);
}
if (!audio_done_) {
// We set the audio backend to render the full sequence to the disk
connect(audio_backend_, &AudioRenderBackend::AudioComplete, this, &Exporter::AudioRendered);
audio_backend_->InvalidateCache(export_range_);
}
}
void Exporter::EncoderOpenFailed()
{
SetExportMessage(tr("Failed to open encoder"));
ExportStopped();
}
void Exporter::EncoderClosed()
{
emit ProgressChanged(100);
ExportStopped();
}
void Exporter::VideoHashesComplete()
{
// We've got our hashes, time to kick off actual rendering
disconnect(video_backend_, &VideoRenderBackend::QueueComplete, this, &Exporter::VideoHashesComplete);
// Determine what frames will be hashed
TimeRangeList ranges;
ranges.append(TimeRange(0, viewer_node_->Length()));
// Set video backend to render mode but NOT hash or download
video_backend_->SetOperatingMode(VideoRenderWorker::kRenderOnly);
video_backend_->SetOnlySignalLastFrameRequested(false);
connect(video_backend_, &VideoRenderBackend::GeneratedFrame, this, &Exporter::FrameRendered);
foreach (const TimeRange& range, ranges) {
video_backend_->InvalidateCache(range);
}
}
void Exporter::DebugTimerMessage()
{
qDebug() << "Still waiting for" << waiting_for_frame_.toDouble();
}
OLIVE_NAMESPACE_EXIT