diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 239822742..cd3554872 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -343,7 +343,7 @@ bool Decoder::HasConformedVersion(const AudioRenderingParams ¶ms) return index_already_matches; } -void Decoder::SignalIndexProgress(const int64_t &ts) +void Decoder::SignalProcessingProgress(const int64_t &ts) { if (stream()->duration() != AV_NOPTS_VALUE && stream()->duration() != 0) { emit IndexProgress(qRound(100.0 * static_cast(ts) / static_cast(stream()->duration()))); diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 5ab6b0c9c..7f273f936 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -245,7 +245,7 @@ signals: void IndexProgress(int); protected: - void SignalIndexProgress(const int64_t& ts); + void SignalProcessingProgress(const int64_t& ts); /** * @brief Returns the filename for the index diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index c8139d0d4..96e4a6ad9 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -27,6 +27,7 @@ extern "C" { #include } +#include #include #include #include @@ -588,8 +589,6 @@ void FFmpegDecoder::Error(const QString &s) bool FFmpegDecoder::ProxyVideo(const QAtomicInt *cancelled, int divider) { - return false; - VideoStreamPtr video_stream = std::static_pointer_cast(stream()); QString frame_index_file = GetIndexFilename().append('d').append(QString::number(divider)); @@ -597,32 +596,113 @@ bool FFmpegDecoder::ProxyVideo(const QAtomicInt *cancelled, int divider) if (QFileInfo::exists(frame_index_file)) { // A proxy of this type already exists so we can do nothing - video_stream->append_proxy(divider); - - } else { - - // Iterate each frame and transcode it to EXR - FFmpegDecoderInstance instance(stream()->footage()->filename().toUtf8(), stream()->index()); - - int ret; - - AVPacket* pkt = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - - while (true) { - ret = instance.GetFrame(pkt, frame); - - if (ret < 0) { - if (ret == AVERROR_EOF) { - - } - } - } - - av_frame_free(&frame); - av_packet_free(&pkt); + video_stream->set_proxy(divider); + return true; } + + // Iterate each frame and transcode it to EXR + FFmpegDecoderInstance instance(stream()->footage()->filename().toUtf8(), stream()->index()); + + int ret; + + AVPixelFormat src_fmt = static_cast(instance.stream()->codecpar->format); + AVPixelFormat ideal_fmt = FFmpegCommon::GetCompatiblePixelFormat(src_fmt); + PixelFormat::Format native_fmt = GetNativePixelFormat(ideal_fmt); + + int divided_width = instance.stream()->codecpar->width; + int divided_height = instance.stream()->codecpar->height; + + SwsContext* scaler = sws_getContext(instance.stream()->codecpar->width, + instance.stream()->codecpar->height, + src_fmt, + divided_width, + divided_height, + ideal_fmt, + SWS_FAST_BILINEAR, + nullptr, + nullptr, + 0); + + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + QVector frame_index; + + QByteArray converted_buffer(PixelFormat::GetBufferSize(native_fmt, + divided_width, + divided_height), + Qt::Uninitialized); + + uint8_t* converted_data = reinterpret_cast(converted_buffer.data()); + int converted_linesize = PixelFormat::GetBufferSize(native_fmt, + divided_width, + 1); + + bool succeeded = false; + + while (true) { + if (cancelled && *cancelled) { + break; + } + + ret = instance.GetFrame(pkt, frame); + + // Handle errors + if (ret < 0) { + if (ret == AVERROR_EOF) { + succeeded = true; + } else { + char err_str[50]; + av_strerror(ret, err_str, 50); + qWarning() << "Failed to proxy:" << ret << err_str; + } + + break; + } + + sws_scale(scaler, + frame->data, + frame->linesize, + 0, + frame->height, + &converted_data, + &converted_linesize); + + QString dst_fn = GetIndexFilename() + .append(QString::number(frame->pts)) + .append(QStringLiteral(".tiff")); + + std::string dst_std_fn = dst_fn.toStdString(); + + auto out = OIIO::ImageOutput::create(dst_std_fn); + + if (out) { + + out->open(dst_std_fn, + OIIO::ImageSpec(divided_width, + divided_height, + PixelFormat::ChannelCount(native_fmt), + PixelFormat::GetOIIOTypeDesc(native_fmt))); + + out->write_image(PixelFormat::GetOIIOTypeDesc(native_fmt), converted_data); + + out->close(); + +#if OIIO_VERSION < 10903 + OIIO::ImageOutput::destroy(out); +#endif + } + + frame_index.append(frame->pts); + SignalProcessingProgress(frame->pts); + } + + sws_freeContext(scaler); + + av_frame_free(&frame); + av_packet_free(&pkt); + + return succeeded; } bool FFmpegDecoder::ConformAudio(const QAtomicInt *cancelled, const AudioRenderingParams &p) @@ -694,39 +774,39 @@ bool FFmpegDecoder::ConformAudio(const QAtomicInt *cancelled, const AudioRenderi } else { char err_str[50]; av_strerror(ret, err_str, 50); - qWarning() << "Failed to index:" << ret << err_str; + qWarning() << "Failed to conform:" << ret << err_str; } break; - } else { - // Allocate buffers - int nb_samples = swr_get_out_samples(resampler, frame->nb_samples); - char* data = new char[p.samples_to_bytes(nb_samples)]; - - // Resample audio to our destination parameters - nb_samples = swr_convert(resampler, - reinterpret_cast(&data), - nb_samples, - const_cast(frame->data), - frame->nb_samples); - - if (nb_samples < 0) { - char err_str[50]; - av_strerror(nb_samples, err_str, 50); - qWarning() << "libswresample failed with error:" << nb_samples << err_str; - break; - } - - // Write packed WAV data to the disk cache - wave_out.write(data, p.samples_to_bytes(nb_samples)); - - // If we allocated an output for the resampler, delete it here - if (data != reinterpret_cast(frame->data[0])) { - delete [] data; - } - - SignalIndexProgress(frame->pts); } + + // Allocate buffers + int nb_samples = swr_get_out_samples(resampler, frame->nb_samples); + char* data = new char[p.samples_to_bytes(nb_samples)]; + + // Resample audio to our destination parameters + nb_samples = swr_convert(resampler, + reinterpret_cast(&data), + nb_samples, + const_cast(frame->data), + frame->nb_samples); + + if (nb_samples < 0) { + char err_str[50]; + av_strerror(nb_samples, err_str, 50); + qWarning() << "libswresample failed with error:" << nb_samples << err_str; + break; + } + + // Write packed WAV data to the disk cache + wave_out.write(data, p.samples_to_bytes(nb_samples)); + + // If we allocated an output for the resampler, delete it here + if (data != reinterpret_cast(frame->data[0])) { + delete [] data; + } + + SignalProcessingProgress(frame->pts); } wave_out.close(); diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 436f38516..4acb16569 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -304,11 +304,11 @@ void Footage::ClearStreams() streams_.clear(); } -bool Footage::HasStreamsOfType(const Stream::Type type) +bool Footage::HasStreamsOfType(const Stream::Type &type) const { // Return true if any streams are video streams - for (int i=0;itype() == type) { + foreach (StreamPtr stream, streams_) { + if (stream->type() == type) { return true; } } @@ -316,6 +316,17 @@ bool Footage::HasStreamsOfType(const Stream::Type type) return false; } +StreamPtr Footage::get_first_stream_of_type(const Stream::Type &type) const +{ + foreach (StreamPtr stream, streams_) { + if (stream->type() == type) { + return stream; + } + } + + return nullptr; +} + void Footage::UpdateTooltip() { switch (status_) { diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index a3a026508..2bcf485a5 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -206,12 +206,6 @@ public: quint64 get_enabled_stream_flags() const; -private: - /** - * @brief Internal function to delete all Stream children and empty the array - */ - void ClearStreams(); - /** * @brief Check if this footage has streams of a certain type * @@ -219,7 +213,15 @@ private: * * The stream type to check for */ - bool HasStreamsOfType(const Stream::Type type); + bool HasStreamsOfType(const Stream::Type& type) const; + + StreamPtr get_first_stream_of_type(const Stream::Type& type) const; + +private: + /** + * @brief Internal function to delete all Stream children and empty the array + */ + void ClearStreams(); /** * @brief Update the icon based on the Footage status diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index 62e942207..240f01b36 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -102,8 +102,6 @@ protected: virtual void SaveCustomParameters(QXmlStreamWriter* writer) const; signals: - void IndexChanged(); - void ParametersChanged(); private: diff --git a/app/project/item/footage/videostream.cpp b/app/project/item/footage/videostream.cpp index 0a90fd46a..25e67f586 100644 --- a/app/project/item/footage/videostream.cpp +++ b/app/project/item/footage/videostream.cpp @@ -30,7 +30,9 @@ const int64_t VideoStream::kEndTimestamp = AV_NOPTS_VALUE; VideoStream::VideoStream() : start_time_(0), - is_image_sequence_(false) + is_image_sequence_(false), + is_generating_proxy_(false), + using_proxy_(0) { set_type(kVideo); } @@ -73,18 +75,39 @@ void VideoStream::set_image_sequence(bool e) is_image_sequence_ = e; } -bool VideoStream::has_proxy(const int ÷r) +bool VideoStream::is_generating_proxy() { QMutexLocker locker(proxy_access_lock()); - return proxies_.contains(divider); + return is_generating_proxy_; } -void VideoStream::append_proxy(const int ÷r) +bool VideoStream::try_start_proxy() { QMutexLocker locker(proxy_access_lock()); - proxies_.append(divider); + if (is_generating_proxy_) { + return false; + } + + is_generating_proxy_ = true; + + return true; +} + +int VideoStream::using_proxy() +{ + QMutexLocker locker(proxy_access_lock()); + + return using_proxy_; +} + +void VideoStream::set_proxy(const int ÷r) +{ + QMutexLocker locker(proxy_access_lock()); + + using_proxy_ = divider; + is_generating_proxy_ = false; } /* diff --git a/app/project/item/footage/videostream.h b/app/project/item/footage/videostream.h index 721df3277..f50093af2 100644 --- a/app/project/item/footage/videostream.h +++ b/app/project/item/footage/videostream.h @@ -61,8 +61,10 @@ public: bool save_frame_index(const QString& s); */ - bool has_proxy(const int& divider); - void append_proxy(const int& divider); + bool is_generating_proxy(); + bool try_start_proxy(); + int using_proxy(); + void set_proxy(const int& divider); private: rational frame_rate_; @@ -73,7 +75,9 @@ private: bool is_image_sequence_; - QVector proxies_; + bool is_generating_proxy_; + + int using_proxy_; }; diff --git a/app/render/backend/audiorenderbackend.cpp b/app/render/backend/audiorenderbackend.cpp index cb48ebb4d..9e819c0c8 100644 --- a/app/render/backend/audiorenderbackend.cpp +++ b/app/render/backend/audiorenderbackend.cpp @@ -65,6 +65,8 @@ void AudioRenderBackend::DisconnectViewer(ViewerOutput *node) { disconnect(node, &ViewerOutput::AudioChangedBetween, this, &AudioRenderBackend::InvalidateCache); disconnect(node, &ViewerOutput::LengthChanged, this, &AudioRenderBackend::TruncateCache); + + conform_wait_info_.clear(); } bool AudioRenderBackend::GenerateCacheIDInternal(QCryptographicHash &hash) diff --git a/app/task/CMakeLists.txt b/app/task/CMakeLists.txt index 914998798..e9d4fa410 100644 --- a/app/task/CMakeLists.txt +++ b/app/task/CMakeLists.txt @@ -15,6 +15,7 @@ # along with this program. If not, see . add_subdirectory(conform) +add_subdirectory(proxy) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/task/conform/conform.cpp b/app/task/conform/conform.cpp index 7202c572b..091c85e35 100644 --- a/app/task/conform/conform.cpp +++ b/app/task/conform/conform.cpp @@ -34,7 +34,7 @@ ConformTask::ConformTask(AudioStreamPtr stream, const AudioRenderingParams& para void ConformTask::Action() { if (stream_->footage()->decoder().isEmpty()) { - emit Failed(QStringLiteral("Stream has no decoder")); + emit Failed(tr("Failed to find decoder to conform audio stream")); } else { DecoderPtr decoder = Decoder::CreateFromID(stream_->footage()->decoder()); @@ -42,9 +42,11 @@ void ConformTask::Action() connect(decoder.get(), &Decoder::IndexProgress, this, &ConformTask::ProgressChanged); - decoder->ConformAudio(&IsCancelled(), params_); - - emit Succeeded(); + if (decoder->ConformAudio(&IsCancelled(), params_)) { + emit Succeeded(); + } else { + emit Failed(QStringLiteral("Failed to conform audio")); + } } } diff --git a/app/task/proxy/CMakeLists.txt b/app/task/proxy/CMakeLists.txt new file mode 100644 index 000000000..6d7ca02aa --- /dev/null +++ b/app/task/proxy/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + task/proxy/proxy.h + task/proxy/proxy.cpp + PARENT_SCOPE +) diff --git a/app/task/proxy/proxy.cpp b/app/task/proxy/proxy.cpp new file mode 100644 index 000000000..f086508c7 --- /dev/null +++ b/app/task/proxy/proxy.cpp @@ -0,0 +1,60 @@ +/*** + + 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 "proxy.h" + +#include "codec/decoder.h" + +OLIVE_NAMESPACE_ENTER + +ProxyTask::ProxyTask(VideoStreamPtr stream, int divider) : + stream_(stream), + divider_(divider) +{ + if (divider_ == 1) { + SetTitle(tr("Generating full resolution proxy %1:%2").arg(stream_->footage()->filename(), + QString::number(stream_->index()))); + } else { + SetTitle(tr("Generating 1/%1 resolution proxy %2:%3").arg(QString::number(divider), + stream_->footage()->filename(), + QString::number(stream_->index()))); + } +} + +void ProxyTask::Action() +{ + if (stream_->footage()->decoder().isEmpty()) { + emit Failed(tr("Failed to find decoder to conform audio stream")); + } else { + DecoderPtr decoder = Decoder::CreateFromID(stream_->footage()->decoder()); + + decoder->set_stream(stream_); + + connect(decoder.get(), &Decoder::IndexProgress, this, &ProxyTask::ProgressChanged); + + if (decoder->ProxyVideo(&IsCancelled(), divider_)) { + emit Succeeded(); + } else { + emit Failed(QStringLiteral("Failed to generate proxy")); + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/task/proxy/proxy.h b/app/task/proxy/proxy.h new file mode 100644 index 000000000..10610f074 --- /dev/null +++ b/app/task/proxy/proxy.h @@ -0,0 +1,46 @@ +/*** + + 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 . + +***/ + +#ifndef PROXYTASK_H +#define PROXYTASK_H + +#include "project/item/footage/videostream.h" +#include "task/task.h" + +OLIVE_NAMESPACE_ENTER + +class ProxyTask : public Task +{ +public: + ProxyTask(VideoStreamPtr stream, int divider); + +protected: + virtual void Action() override; + +private: + VideoStreamPtr stream_; + + int divider_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PROXYTASK_H diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 29fb5fbbc..ae86d1092 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -31,6 +31,8 @@ #include "core.h" #include "dialog/footageproperties/footageproperties.h" #include "dialog/sequence/sequence.h" +#include "task/proxy/proxy.h" +#include "task/taskmanager.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" @@ -284,6 +286,36 @@ void ProjectExplorer::ShowContextMenu() connect(reveal_action, &QAction::triggered, this, &ProjectExplorer::RevealSelectedFootage); menu.addSeparator(); + + Footage* f = static_cast(context_menu_item_); + + if (f->HasStreamsOfType(Stream::kVideo)) { + Menu* proxy_menu = new Menu(tr("Proxy"), &menu); + menu.addMenu(proxy_menu); + + VideoStreamPtr video_stream = std::static_pointer_cast(f->get_first_stream_of_type(Stream::kVideo)); + + if (video_stream->is_generating_proxy()) { + + // Prevent multiple proxy actions from occurring at once + QAction* cant_proxy_action = proxy_menu->addAction(tr("Proxy being generated...")); + cant_proxy_action->setEnabled(false); + + } else { + + proxy_menu->addAction(tr("(None)"))->setData(0); + proxy_menu->addSeparator(); + proxy_menu->addAction(tr("Full"))->setData(1); + proxy_menu->addAction(tr("1/2"))->setData(2); + proxy_menu->addAction(tr("1/4"))->setData(4); + proxy_menu->addAction(tr("1/8"))->setData(8); + + connect(proxy_menu, &Menu::triggered, this, &ProjectExplorer::ContextMenuStartProxy); + + } + + menu.addSeparator(); + } } QAction* properties_action = menu.addAction(tr("P&roperties")); @@ -347,6 +379,28 @@ void ProjectExplorer::OpenContextMenuItemInNewWindow() Core::instance()->main_window()->FolderOpen(project(), context_menu_item_, true); } +void ProjectExplorer::ContextMenuStartProxy(QAction *a) +{ + // Find video stream + VideoStreamPtr video_stream = nullptr; + + foreach (StreamPtr s, static_cast(context_menu_item_)->streams()) { + if (s->type() == Stream::kVideo) { + video_stream = std::static_pointer_cast(s); + break; + } + } + + if (!video_stream) { + return; + } + + if (video_stream->try_start_proxy()) { + ProxyTask* proxy_task = new ProxyTask(video_stream, a->data().toInt()); + TaskManager::instance()->AddTask(proxy_task); + } +} + Project *ProjectExplorer::project() const { return model_.project(); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index b5219b84d..69bce4b49 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -171,6 +171,8 @@ private slots: void OpenContextMenuItemInNewWindow(); + void ContextMenuStartProxy(QAction* a); + }; OLIVE_NAMESPACE_EXIT