From c779ec43e7fdc2acf0df3efc13149d3f3206d9ed Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 31 Jan 2019 23:01:28 +1100 Subject: [PATCH 1/4] backend transcoding done for proxies --- dialogs/demonotice.cpp | 2 +- dialogs/proxydialog.cpp | 62 ++++++- dialogs/proxydialog.h | 14 +- io/exportthread.cpp | 4 +- io/path.cpp | 9 + io/path.h | 3 + io/previewgenerator.cpp | 25 +-- io/proxygenerator.cpp | 340 ++++++++++++++++++++++++++++++++++++++ io/proxygenerator.h | 35 ++++ mainwindow.cpp | 12 +- olive.pro | 6 +- project/sourcescommon.cpp | 10 +- project/sourcescommon.h | 6 + 13 files changed, 493 insertions(+), 35 deletions(-) create mode 100644 io/proxygenerator.cpp create mode 100644 io/proxygenerator.h diff --git a/dialogs/demonotice.cpp b/dialogs/demonotice.cpp index 9f3055426..7dc0e609e 100644 --- a/dialogs/demonotice.cpp +++ b/dialogs/demonotice.cpp @@ -12,7 +12,7 @@ DemoNotice::DemoNotice(QWidget *parent) : QVBoxLayout* vlayout = new QVBoxLayout(this); - QHBoxLayout* layout = new QHBoxLayout(this); + QHBoxLayout* layout = new QHBoxLayout(); layout->setMargin(10); layout->setSpacing(20); diff --git a/dialogs/proxydialog.cpp b/dialogs/proxydialog.cpp index bf3233a8b..1592ad904 100644 --- a/dialogs/proxydialog.cpp +++ b/dialogs/proxydialog.cpp @@ -5,8 +5,17 @@ #include #include #include +#include -ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : QDialog(parent) { +#include + +#include "io/proxygenerator.h" +#include "project/footage.h" + +ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : + QDialog(parent), + selected_footage(footage) +{ // set dialog title setWindowTitle(tr("Create Proxy")); @@ -19,7 +28,7 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : Q // set the video dimensions of the proxy layout->addWidget(new QLabel(tr("Dimensions:"), this), 0, 0); - QComboBox* size_combobox = new QComboBox(this); + size_combobox = new QComboBox(this); size_combobox->addItem(tr("Same Size as Source"), 1.0); size_combobox->addItem(tr("Half Resolution (1/2)"), 0.5); size_combobox->addItem(tr("Quarter Resolution (1/4)"), 0.25); @@ -30,7 +39,7 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : Q // set the desired format of the proxy to create layout->addWidget(new QLabel(tr("Format:"), this), 1, 0); - QComboBox* format_combobox = new QComboBox(this); + format_combobox = new QComboBox(this); format_combobox->addItem(tr("ProRes HQ")); format_combobox->addItem(tr("ProRes SQ")); format_combobox->addItem(tr("ProRes LT")); @@ -58,6 +67,53 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : Q connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); } +void ProxyDialog::accept() { + QVector info_list; + + for (int i=0;icurrentData().toDouble(); + + QString base_footage_fn = QFileInfo(selected_footage.at(i)->url).fileName(); + + // determine path from input + if (custom_location.isEmpty()) { + // use same as source (proxy subfolder) + + // generate full path from footage path and proxy_folder_name's translated "Proxy" + info.path = QDir(QFileInfo(selected_footage.at(i)->url).dir().filePath(proxy_folder_name)).filePath(base_footage_fn); + } else { + // use existing location + info.path = QDir(custom_location).filePath(base_footage_fn); + } + + // if the proposed proxy file already exists + if (QFileInfo::exists(info.path) && QMessageBox::warning(this, + tr("Proxy file exists"), + tr("The file \"%1\" already exists. Do you wish to replace it?").arg(info.path), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + // return to dialog without closing or starting any proxy generation + return; + } + + // send to proxy generator thread + info_list.append(info); + } + + // all proxy info checks out, queue it with the proxy generator + for (int i=0;i& footage); +public slots: + // called if user clicks "OK" on the dialog + virtual void accept() override; private: - // user's dimensions + // user's desired dimensions QComboBox* size_combobox; + // user's desired proxy format + QComboBox* format_combobox; + // allows users to set the location to store proxies QComboBox* location_combobox; // stores the custom location to store proxies if the user sets a custom location QString custom_location; - // stores the subdirectory to be made next to the source in the user's language + // stores the subdirectory to be made next to the source (dependent on the user's language) QString proxy_folder_name; + + // list of footage to make proxies for + QVector selected_footage; private slots: + // triggered when the user changes the index in the location combobox void location_changed(int i); }; diff --git a/io/exportthread.cpp b/io/exportthread.cpp index 79136b2f5..1f94a1b47 100644 --- a/io/exportthread.cpp +++ b/io/exportthread.cpp @@ -83,7 +83,7 @@ bool ExportThread::setupVideo() { if (!video_enabled) return true; // find video encoder - vcodec = avcodec_find_encoder((enum AVCodecID) video_codec); + vcodec = avcodec_find_encoder(static_cast(video_codec)); if (!vcodec) { qCritical() << "Could not find video encoder"; ed->export_error = tr("could not video encoder for %1").arg(QString::number(video_codec)); @@ -109,7 +109,7 @@ bool ExportThread::setupVideo() { } // setup context - vcodec_ctx->codec_id = static_cast(video_codec); + vcodec_ctx->codec_id = static_cast(video_codec); vcodec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; vcodec_ctx->width = video_width; vcodec_ctx->height = video_height; diff --git a/io/path.cpp b/io/path.cpp index 79248904d..c3a24f84a 100644 --- a/io/path.cpp +++ b/io/path.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include + #include "debug.h" QString real_app_dir; @@ -41,3 +44,9 @@ QList get_effects_paths() { if (!env_path.isEmpty()) effects_paths.append(env_path); return effects_paths; } + +QString get_file_hash(const QString& filename) { + QFileInfo file_info(filename); + QString cache_file = filename.mid(filename.lastIndexOf('/')+1) + QString::number(file_info.size()) + QString::number(file_info.lastModified().toMSecsSinceEpoch()); + return QCryptographicHash::hash(cache_file.toUtf8(), QCryptographicHash::Md5).toHex(); +} diff --git a/io/path.h b/io/path.h index 2e2f6d841..2a721c0fd 100644 --- a/io/path.h +++ b/io/path.h @@ -8,4 +8,7 @@ QString get_data_path(); QString get_config_path(); QList get_effects_paths(); +// generate hash algorithm used to uniquely identify files +QString get_file_hash(const QString& filename); + #endif // PATH_H diff --git a/io/previewgenerator.cpp b/io/previewgenerator.cpp index ca2fd3bbe..892367bf5 100644 --- a/io/previewgenerator.cpp +++ b/io/previewgenerator.cpp @@ -13,10 +13,8 @@ #include #include #include -#include #include #include -#include #define WAVEFORM_RESOLUTION 64 #define THUMBNAIL_RESOLUTION 120 @@ -68,13 +66,7 @@ void PreviewGenerator::parse_media() { && fmt_ctx->streams[i]->codecpar->width > 0 && fmt_ctx->streams[i]->codecpar->height > 0) { - /*dout << "avg_frame_rate was:" << fmt_ctx->streams[i]->avg_frame_rate.num << "/" << fmt_ctx->streams[i]->avg_frame_rate.den; - dout << "r_frame_rate was:" << fmt_ctx->streams[i]->r_frame_rate.num << "/" << fmt_ctx->streams[i]->r_frame_rate.den; - dout << "codec_frame_rate was:" << fmt_ctx->streams[i]->codec->framerate.num << "/" << fmt_ctx->streams[i]->codec->framerate.den; - dout << "nb_frames was:" << fmt_ctx->streams[i]->nb_frames; - dout << "duration was:" << fmt_ctx->streams[i]->duration << "OR fmt_ctx's duration is:" << fmt_ctx->duration;*/ - - // heuristic to determine if video is a still image + // heuristic to determine if video is a still image (if it is, we treat it differently in the playback/render process) if (fmt_ctx->streams[i]->avg_frame_rate.den == 0 && fmt_ctx->streams[i]->codecpar->codec_id != AV_CODEC_ID_DNXHD) { // silly hack but this is the only scenario i've seen this if (footage->url.contains('%')) { @@ -85,16 +77,10 @@ void PreviewGenerator::parse_media() { contains_still_image = true; ms.video_frame_rate = 0; } + } else { // using ffmpeg's built-in heuristic ms.video_frame_rate = av_q2d(av_guess_frame_rate(fmt_ctx, fmt_ctx->streams[i], nullptr)); - - // old heuristic - /*if (fmt_ctx->streams[i]->r_frame_rate.den == 0) { - ms.video_frame_rate = av_q2d(fmt_ctx->streams[i]->avg_frame_rate); - } else { - ms.video_frame_rate = av_q2d(fmt_ctx->streams[i]->r_frame_rate); - }*/ } ms.video_width = fmt_ctx->streams[i]->codecpar->width; @@ -241,7 +227,9 @@ void PreviewGenerator::generate_waveform() { } } + // TODO may be unnecessary - doesn't av_read_frame allocate a packet itself? AVPacket* packet = av_packet_alloc(); + bool done = true; bool end_of_file = false; @@ -458,10 +446,7 @@ void PreviewGenerator::run() { parse_media(); // see if we already have data for this - QFileInfo file_info(footage->url); - QString cache_file = footage->url.mid(footage->url.lastIndexOf('/')+1) + QString::number(file_info.size()) + QString::number(file_info.lastModified().toMSecsSinceEpoch()); - //dout << "using hash" << cache_file; - QString hash = QCryptographicHash::hash(cache_file.toUtf8(), QCryptographicHash::Md5).toHex(); + QString hash = get_file_hash(footage->url); if (retrieve_preview(hash)) { sem.acquire(); diff --git a/io/proxygenerator.cpp b/io/proxygenerator.cpp new file mode 100644 index 000000000..ca42efdd9 --- /dev/null +++ b/io/proxygenerator.cpp @@ -0,0 +1,340 @@ +#include "proxygenerator.h" + +#include "project/footage.h" +#include "io/path.h" + +#include +#include +#include + +#include + +extern "C" { + #include + #include + #include +} + +enum AVCodecID temp_enc_codec = AV_CODEC_ID_PRORES; + +ProxyGenerator::ProxyGenerator() : cancelled(false) {} + +void transcode(const ProxyInfo& info) { + // open input file + AVFormatContext* input_fmt_ctx = nullptr; + avformat_open_input(&input_fmt_ctx, info.footage->url.toUtf8(), nullptr, nullptr); + + // open output file + AVFormatContext* output_fmt_ctx = nullptr; + avformat_alloc_output_context2(&output_fmt_ctx, nullptr, nullptr, info.path.toUtf8()); + + // open output file writing handle + avio_open(&output_fmt_ctx->pb, info.path.toUtf8(), AVIO_FLAG_WRITE); + + // get stream info from input file + avformat_find_stream_info(input_fmt_ctx, nullptr); + + // create array of input decoders + QVector input_streams; + input_streams.resize(input_fmt_ctx->nb_streams); + input_streams.fill(nullptr); + + // create array of output encoders + QVector output_streams; + output_streams.resize(input_fmt_ctx->nb_streams); + output_streams.fill(nullptr); + + // create array of swscale contexts for pixel format conversion + QVector sws_contexts; + sws_contexts.resize(input_fmt_ctx->nb_streams); + sws_contexts.fill(nullptr); + + // loop through file to find compatible video streams + for (int i=0;inb_streams);i++) { + AVStream* in_stream = input_fmt_ctx->streams[i]; + + // create new stream in output + AVStream* out_stream = avformat_new_stream(output_fmt_ctx, nullptr); + out_stream->id = in_stream->id; + + // find decoder for this codec + AVCodec* dec_codec = avcodec_find_decoder(in_stream->codecpar->codec_id); + + // find encoder for chosen proxy type + AVCodec* enc_codec = avcodec_find_encoder(temp_enc_codec); + + // we only transcode video streams, others we just passthrough + if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && dec_codec != nullptr) { + + // allocate decoding context for this stream + AVCodecContext* dec_ctx = avcodec_alloc_context3(dec_codec); + + // copy parameters from stream to decoding context + avcodec_parameters_to_context(dec_ctx, in_stream->codecpar); + + // open decoder + avcodec_open2(dec_ctx, dec_codec, nullptr); + + // store decoding context in array + input_streams[i] = dec_ctx; + + // retrieve more information about this stream + av_dump_format(input_fmt_ctx, i, info.footage->url.toUtf8(), 0); + + // allocate encoding context for this stream + AVCodecContext* enc_ctx = avcodec_alloc_context3(enc_codec); + + // copy properties from decoding context to encoding context + enc_ctx->codec_id = temp_enc_codec; + enc_ctx->codec_type = AVMEDIA_TYPE_VIDEO; + enc_ctx->width = qFloor(dec_ctx->width*info.size_multiplier); + enc_ctx->height = qFloor(dec_ctx->height*info.size_multiplier); + enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio; + enc_ctx->pix_fmt = enc_codec->pix_fmts[0]; + enc_ctx->framerate = dec_ctx->framerate; + enc_ctx->time_base = in_stream->time_base; + + out_stream->time_base = in_stream->time_base; + + // if format uses global headers, add flag to enc_ctx + if (output_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) { + enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + // set encoder options (mostly just multithreading) + AVDictionary* opts = nullptr; + av_dict_set(&opts, "threads", "auto", 0); + + // open encoder + avcodec_open2(enc_ctx, enc_codec, &opts); + + // copy parameters from encoding context to stream + avcodec_parameters_from_context(out_stream->codecpar, enc_ctx); + + // store encoding context in array + output_streams[i] = enc_ctx; + + // create swscontext for this stream + SwsContext* sws_ctx = sws_getContext( + in_stream->codecpar->width, + in_stream->codecpar->height, + static_cast(in_stream->codecpar->format), + enc_ctx->width, + enc_ctx->height, + enc_ctx->pix_fmt, + 0, + nullptr, + nullptr, + nullptr + ); + + sws_contexts[i] = sws_ctx; + } else { + avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar); + } + } + + // write video header + avformat_write_header(output_fmt_ctx, nullptr); + + // packet that av_read_frame will dump file packets into + AVPacket packet; + av_init_packet(&packet); + + // frame that decoder will decode into + AVFrame* dec_frame = av_frame_alloc(); + + // main transcoding loop + while (true) { + + // cache stream index + int stream_index = packet.stream_index; + + // retrieve frame from decoder (this will clear the last frame so we don't have to do that) + int read_ret = -1; + int recfr_ret = -1; + do { + // read from input file + read_ret = av_read_frame(input_fmt_ctx, &packet); + + // handle errors + if (read_ret < 0) { + + // AVERROR_EOF means we've simply reached the end of the file, otherwise this is an error + if (read_ret != AVERROR_EOF) { + qWarning() << "Proxy generation for file" << info.footage->url << "ended prematurely"; + } + + // either way, we shall abort reading + break; + } + + stream_index = packet.stream_index; + + // determine whether this frame is from a stream we're transcoding + if (input_streams.at(stream_index) == nullptr) { + // if we didn't allocate a decoder for this earlier, we just pass it through + + av_packet_rescale_ts(&packet, input_fmt_ctx->streams[stream_index]->time_base, output_fmt_ctx->streams[stream_index]->time_base); + + // write packet to output + av_interleaved_write_frame(output_fmt_ctx, &packet); + + } else { + // we're going to transcode this packet. + + // send packet to decoder + avcodec_send_packet(input_streams.at(stream_index), &packet); + + } + + // free packet allocated by av_read_frame + av_packet_unref(&packet); + } while ((recfr_ret = avcodec_receive_frame(input_streams.at(packet.stream_index), dec_frame)) == AVERROR(EAGAIN)); + + // error/eof handling - cancel while loop + if (read_ret < 0) { + break; + } + + // + // SWSCALE IF NECESSARY + // + + // free packet as we're about to use it for encoding + av_packet_unref(&packet); + + av_rescale_q(dec_frame->pts, input_fmt_ctx->streams[stream_index]->time_base, output_fmt_ctx->streams[stream_index]->time_base); + + bool convert_pix_fmt = (output_streams.at(stream_index)->pix_fmt != input_streams.at(stream_index)->pix_fmt); + + AVFrame* enc_frame = dec_frame; + + if (convert_pix_fmt) { + // create sws frame for pixel format conversion + enc_frame = av_frame_alloc(); + enc_frame->width = output_streams.at(stream_index)->width; + enc_frame->height = output_streams.at(stream_index)->height; + enc_frame->format = output_streams.at(stream_index)->pix_fmt; + av_frame_get_buffer(enc_frame, 0); + + // convert pixel format to format expected by the encoder + sws_scale(sws_contexts.at(stream_index), dec_frame->data, dec_frame->linesize, 0, dec_frame->height, enc_frame->data, enc_frame->linesize); + + // set same pts as dec_frame + enc_frame->pts = dec_frame->pts; + } + + // send frame to encoder + avcodec_send_frame(output_streams.at(stream_index), enc_frame); + + if (convert_pix_fmt) { + // free sws frame since we made one before + av_frame_free(&enc_frame); + } + + int recret; + while ((recret = avcodec_receive_packet(output_streams.at(stream_index), &packet)) >= 0) { + + packet.stream_index = stream_index; + + av_interleaved_write_frame(output_fmt_ctx, &packet); + + av_packet_unref(&packet); + } + + } + + // free dec_frame + av_frame_free(&dec_frame); + + // write video trailer + av_write_trailer(output_fmt_ctx); + + // free stream contexts + for (int i=0;inb_streams);i++) { + if (input_streams[i] != nullptr) { + // free swscale contexts + sws_freeContext(sws_contexts[i]); + + // free input decoding context + avcodec_close(input_streams[i]); + avcodec_free_context(&input_streams[i]); + + // free output encoding context + avcodec_close(output_streams[i]); + avcodec_free_context(&output_streams[i]); + } + } + + // close output file handle + avio_closep(&output_fmt_ctx->pb); + + // close output file + avformat_free_context(output_fmt_ctx); + + // close input file + avformat_close_input(&input_fmt_ctx); + + qInfo() << "Finished creating proxy for" << info.footage->url; +} + +// main proxy generating loop +void ProxyGenerator::run() { + // mutex used for thread safe signalling + mutex.lock(); + + while (!cancelled) { + // wait for queue() to be called + waitCond.wait(&mutex); + + // quit thread if cancel() was called + if (cancelled) break; + + // loop through queue until the queue is empty + while (proxy_queue.size() > 0) { + + // grab proxy info + const ProxyInfo& info = proxy_queue.first(); + + // create directory for info + QFileInfo(info.path).dir().mkpath("."); + + // transcode proxy + transcode(info); + + // we're finished with this proxy, remove it + proxy_queue.removeFirst(); + + // quit loop if cancel() was called + if (cancelled) break; + + } + } + + mutex.unlock(); +} + +// called to add footage to generate proxies for +void ProxyGenerator::queue(const ProxyInfo &info) { + // add proxy info to queue + proxy_queue.append(info); + + // wake proxy thread loop if sleeping + waitCond.wakeAll(); +} + +// to be called from another thread to terminate the proxy generator thread and free it +void ProxyGenerator::cancel() { + // signal to thread to cancel + cancelled = true; + + // if signal is sleeping, wake it to cancel correctly + waitCond.wakeAll(); + + // wait for thread to finish + wait(); +} + +// proxy generator is a global omnipotent entity +ProxyGenerator proxy_generator; diff --git a/io/proxygenerator.h b/io/proxygenerator.h new file mode 100644 index 000000000..667887a20 --- /dev/null +++ b/io/proxygenerator.h @@ -0,0 +1,35 @@ +#ifndef PROXYGENERATOR_H +#define PROXYGENERATOR_H + +#include +#include +#include +#include + +struct Footage; + +struct ProxyInfo { + Footage* footage; + double size_multiplier; + int codec_type; + QString path; +}; + +class ProxyGenerator : public QThread +{ +public: + ProxyGenerator(); + void run(); + void queue(const ProxyInfo& info); + void cancel(); +private: + QVector proxy_queue; + QWaitCondition waitCond; + QMutex mutex; + bool cancelled; +}; + +// proxy generator is a global omnipotent entity +extern ProxyGenerator proxy_generator; + +#endif // PROXYGENERATOR_H diff --git a/mainwindow.cpp b/mainwindow.cpp index 042c52f7a..bd9db29d3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2,6 +2,7 @@ #include "io/config.h" #include "io/path.h" +#include "io/proxygenerator.h" #include "project/footage.h" #include "project/sequence.h" @@ -211,6 +212,7 @@ MainWindow::MainWindow(QWidget *parent, const QString &an) : statusBar->showMessage("Welcome to " + appName); setStatusBar(statusBar); + // populate menu bars setup_menus(); if (!data_dir.isEmpty()) { @@ -227,9 +229,14 @@ MainWindow::MainWindow(QWidget *parent, const QString &an) : autorecovery_timer.start(); } + // set up panel layout setup_layout(false); + // set up output audio device init_audio(); + + // start omnipotent proxy generator process + proxy_generator.start(); } MainWindow::~MainWindow() { @@ -988,6 +995,9 @@ void MainWindow::updateTitle(const QString& url) { void MainWindow::closeEvent(QCloseEvent *e) { if (can_close_project()) { + // stop proxy generator thread + proxy_generator.cancel(); + panel_effect_controls->clear_effects(true); set_sequence(nullptr); @@ -1037,8 +1047,8 @@ void MainWindow::paintEvent(QPaintEvent *event) { if (!demoNoticeShown) { #ifndef QT_DEBUG DemoNotice* d = new DemoNotice(this); + connect(d, SIGNAL(finished(int)), d, SLOT(deleteLater())); d->open(); - connect(d, SIGNAL(finished()), d, SLOT(deleteLater())); #endif demoNoticeShown = true; diff --git a/olive.pro b/olive.pro index 334fafcb9..ca8c48050 100644 --- a/olive.pro +++ b/olive.pro @@ -138,7 +138,8 @@ SOURCES += \ io/crossplatformlib.cpp \ effects/internal/vsthost.cpp \ ui/flowlayout.cpp \ - dialogs/proxydialog.cpp + dialogs/proxydialog.cpp \ + io/proxygenerator.cpp HEADERS += \ mainwindow.h \ @@ -240,7 +241,8 @@ HEADERS += \ io/crossplatformlib.h \ effects/internal/vsthost.h \ ui/flowlayout.h \ - dialogs/proxydialog.h + dialogs/proxydialog.h \ + io/proxygenerator.h FORMS += diff --git a/project/sourcescommon.cpp b/project/sourcescommon.cpp index 18462b889..957eb5b7a 100644 --- a/project/sourcescommon.cpp +++ b/project/sourcescommon.cpp @@ -103,11 +103,14 @@ void SourcesCommon::show_context_menu(QWidget* parent, const QModelIndexList& it // duplicate item bool all_sequences = true; bool all_footage = true; + cached_selected_footage.clear(); for (int i=0;iget_type() != MEDIA_TYPE_SEQUENCE) { all_sequences = false; } - if (m->get_type() != MEDIA_TYPE_FOOTAGE) { + if (m->get_type() == MEDIA_TYPE_FOOTAGE) { + cached_selected_footage.append(m->to_footage()); + } else { all_footage = false; } } @@ -301,8 +304,7 @@ void SourcesCommon::item_renamed(Media* item) { } void SourcesCommon::open_create_proxy_dialog() { - QVector selected_footage; - - ProxyDialog pd(mainWindow, selected_footage); + // open the proxy dialog and send it a list of currently selected footage + ProxyDialog pd(mainWindow, cached_selected_footage); pd.exec(); } diff --git a/project/sourcescommon.h b/project/sourcescommon.h index aa740f1d9..3c15e5ae2 100644 --- a/project/sourcescommon.h +++ b/project/sourcescommon.h @@ -3,6 +3,7 @@ #include #include +#include class Project; class QMouseEvent; @@ -10,6 +11,8 @@ class Media; class QAbstractItemView; class QDropEvent; +struct Footage; + class SourcesCommon : public QObject { Q_OBJECT public: @@ -35,6 +38,9 @@ private: Project* project_parent; void stop_rename_timer(); QTimer rename_timer; + + // we cache the selected footage items for open_create_proxy_dialog() + QVector cached_selected_footage; }; #endif // SOURCESCOMMON_H From 7a22065162b2625dd8f365a2738ef44f81bc3308 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 1 Feb 2019 01:17:22 +1100 Subject: [PATCH 2/4] most proxy behavior is complete --- dialogs/proxydialog.cpp | 3 ++ io/config.h | 2 +- io/loadthread.cpp | 4 ++ io/proxygenerator.cpp | 65 +++++++++++++++++++++---- io/proxygenerator.h | 15 ++++++ panels/project.cpp | 4 ++ playback/cacher.cpp | 14 +++++- playback/playback.cpp | 50 +++++++++++++++++-- project/footage.cpp | 3 +- project/footage.h | 4 ++ project/sourcescommon.cpp | 100 +++++++++++++++++++++++++++++++++++--- project/sourcescommon.h | 3 ++ ui/renderthread.cpp | 1 - 13 files changed, 245 insertions(+), 23 deletions(-) diff --git a/dialogs/proxydialog.cpp b/dialogs/proxydialog.cpp index 1592ad904..5d7b47469 100644 --- a/dialogs/proxydialog.cpp +++ b/dialogs/proxydialog.cpp @@ -108,6 +108,9 @@ void ProxyDialog::accept() { // all proxy info checks out, queue it with the proxy generator for (int i=0;iproxy = true; + info_list.at(i).footage->proxy_path.clear(); + proxy_generator.queue(info_list.at(i)); } diff --git a/io/config.h b/io/config.h index 5e8c61f27..c01ee0ee3 100644 --- a/io/config.h +++ b/io/config.h @@ -3,7 +3,7 @@ #include -#define SAVE_VERSION 190120 // YYMMDD +#define SAVE_VERSION 190201 // YYMMDD #define MIN_SAVE_VERSION 190104 // lowest compatible project version #define TIMECODE_DROP 0 diff --git a/io/loadthread.cpp b/io/loadthread.cpp index adfc3a510..068ed901a 100644 --- a/io/loadthread.cpp +++ b/io/loadthread.cpp @@ -250,6 +250,10 @@ bool LoadThread::load_worker(QFile& f, QXmlStreamReader& stream, int type) { m->speed = attr.value().toDouble(); } else if (attr.name() == "alphapremul") { m->alpha_is_premultiplied = (attr.value() == "1"); + } else if (attr.name() == "proxy") { + m->proxy = (attr.value() == "1"); + } else if (attr.name() == "proxypath") { + m->proxy_path = attr.value().toString(); } } diff --git a/io/proxygenerator.cpp b/io/proxygenerator.cpp index ca42efdd9..9f54b56a5 100644 --- a/io/proxygenerator.cpp +++ b/io/proxygenerator.cpp @@ -2,10 +2,12 @@ #include "project/footage.h" #include "io/path.h" +#include "mainwindow.h" #include #include #include +#include #include @@ -19,7 +21,10 @@ enum AVCodecID temp_enc_codec = AV_CODEC_ID_PRORES; ProxyGenerator::ProxyGenerator() : cancelled(false) {} -void transcode(const ProxyInfo& info) { +void ProxyGenerator::transcode(const ProxyInfo& info) { + // set progress to 0 + current_progress = 0.0; + // open input file AVFormatContext* input_fmt_ctx = nullptr; avformat_open_input(&input_fmt_ctx, info.footage->url.toUtf8(), nullptr, nullptr); @@ -145,7 +150,7 @@ void transcode(const ProxyInfo& info) { AVFrame* dec_frame = av_frame_alloc(); // main transcoding loop - while (true) { + while (!skip) { // cache stream index int stream_index = packet.stream_index; @@ -186,28 +191,30 @@ void transcode(const ProxyInfo& info) { // send packet to decoder avcodec_send_packet(input_streams.at(stream_index), &packet); + // use timestamp and stream duration to create a rough estimation of the progress through this file + current_progress = qCeil((double(packet.pts)/double(input_fmt_ctx->streams[packet.stream_index]->duration))*100); + } // free packet allocated by av_read_frame av_packet_unref(&packet); - } while ((recfr_ret = avcodec_receive_frame(input_streams.at(packet.stream_index), dec_frame)) == AVERROR(EAGAIN)); + } while ((recfr_ret = avcodec_receive_frame(input_streams.at(packet.stream_index), dec_frame)) == AVERROR(EAGAIN) && !skip); // error/eof handling - cancel while loop - if (read_ret < 0) { + if (read_ret < 0 || skip) { break; } - // - // SWSCALE IF NECESSARY - // - // free packet as we're about to use it for encoding av_packet_unref(&packet); + // rescale input frame timestamp to output timestamp av_rescale_q(dec_frame->pts, input_fmt_ctx->streams[stream_index]->time_base, output_fmt_ctx->streams[stream_index]->time_base); + // determine if the pix_fmt is different, so if we need to convert bool convert_pix_fmt = (output_streams.at(stream_index)->pix_fmt != input_streams.at(stream_index)->pix_fmt); + // create reference to the frame to be sent to the encoder AVFrame* enc_frame = dec_frame; if (convert_pix_fmt) { @@ -233,14 +240,21 @@ void transcode(const ProxyInfo& info) { av_frame_free(&enc_frame); } + // return value for packet receiving int recret; - while ((recret = avcodec_receive_packet(output_streams.at(stream_index), &packet)) >= 0) { + // loop through receiving packets + while ((recret = avcodec_receive_packet(output_streams.at(stream_index), &packet)) >= 0 && !skip) { + + // set packet stream index to current stream index packet.stream_index = stream_index; + // write frame to file av_interleaved_write_frame(output_fmt_ctx, &packet); + // unref old packet av_packet_unref(&packet); + } } @@ -276,7 +290,13 @@ void transcode(const ProxyInfo& info) { // close input file avformat_close_input(&input_fmt_ctx); + // set footage to use newly generated proxy + info.footage->proxy = true; + info.footage->proxy_path = info.path; + qInfo() << "Finished creating proxy for" << info.footage->url; + mainWindow->statusBar()->showMessage(tr("Finished generating proxy for \"%1\"").arg(info.footage->url)); + } // main proxy generating loop @@ -300,6 +320,9 @@ void ProxyGenerator::run() { // create directory for info QFileInfo(info.path).dir().mkpath("."); + // set skip to false + skip = false; + // transcode proxy transcode(info); @@ -317,6 +340,22 @@ void ProxyGenerator::run() { // called to add footage to generate proxies for void ProxyGenerator::queue(const ProxyInfo &info) { + // remove any queued proxies with the same footage + if (!proxy_queue.isEmpty() + && proxy_queue.first().footage == info.footage) { + // if the thread is currently processing a proxy with the same footage, abort it + skip = true; + } + + // scan through the rest of the queue for another proxy with the same footage (start with 1 since we already processed first()) + for (int i=1;i proxy_queue; + + // threading objects QWaitCondition waitCond; QMutex mutex; + + // set to true if you want to permanently close ProxyGenerator bool cancelled; + + // set to true if you want to abort the footage currently being processed + bool skip; + + // stores progress in percent of proxy currently being processed + double current_progress; + + // function that performs the actual transcode + void transcode(const ProxyInfo& info); }; // proxy generator is a global omnipotent entity diff --git a/panels/project.cpp b/panels/project.cpp index 4ec6ae81a..0c543e1da 100644 --- a/panels/project.cpp +++ b/panels/project.cpp @@ -954,6 +954,10 @@ void Project::save_folder(QXmlStreamWriter& stream, int type, bool set_ids_only, stream.writeAttribute("out", QString::number(f->out)); stream.writeAttribute("speed", QString::number(f->speed)); stream.writeAttribute("alphapremul", QString::number(f->alpha_is_premultiplied)); + + stream.writeAttribute("proxy", QString::number(f->proxy)); + stream.writeAttribute("proxypath", f->proxy_path); + for (int j=0;jvideo_tracks.size();j++) { const FootageStream& ms = f->video_tracks.at(j); stream.writeStartElement("video"); diff --git a/playback/cacher.cpp b/playback/cacher.cpp index ae08fbb69..0f29b290b 100644 --- a/playback/cacher.cpp +++ b/playback/cacher.cpp @@ -673,7 +673,19 @@ void open_clip_worker(Clip* clip) { } else if (clip->media->get_type() == MEDIA_TYPE_FOOTAGE) { // opens file resource for FFmpeg and prepares Clip struct for playback Footage* m = clip->media->to_footage(); - QByteArray ba = m->url.toUtf8(); + + // byte array for retriving raw bytes from QString URL + QByteArray ba; + + // do we have a proxy? + if (m->proxy + && !m->proxy_path.isEmpty() + && QFileInfo::exists(m->proxy_path)) { + ba = m->proxy_path.toUtf8(); + } else { + ba = m->url.toUtf8(); + } + const char* filename = ba.constData(); const FootageStream* ms = m->get_stream_from_file_index(clip->track < 0, clip->media_stream); diff --git a/playback/playback.cpp b/playback/playback.cpp index 744723c4e..ce0f1d2d6 100644 --- a/playback/playback.cpp +++ b/playback/playback.cpp @@ -13,6 +13,7 @@ #include "project/media.h" #include "io/config.h" #include "io/avtogl.h" +#include "io/proxygenerator.h" #include "debug.h" extern "C" { @@ -28,6 +29,8 @@ extern "C" { #include #include #include +#include +#include #ifdef QT_DEBUG //#define GCF_DEBUG @@ -272,14 +275,55 @@ void get_clip_frame(Clip* c, long playhead, bool& texture_failed) { uint8_t* data_buffer_1 = target_frame->data[0]; uint8_t* data_buffer_2 = nullptr; - size_t frame_size; + size_t frame_size = size_t(target_frame->linesize[0])*size_t(target_frame->height); + + // if proxy is currently being generated, show an on-screen message of its progress + if (c->media->to_footage()->proxy + && c->media->to_footage()->proxy_path.isEmpty()) { + // create buffers to draw on + data_buffer_1 = new uint8_t[frame_size]; + data_buffer_2 = new uint8_t[frame_size]; + + memcpy(data_buffer_1, target_frame->data[0], frame_size); + + // wrap data in a QImage for painting + QImage img(data_buffer_1, target_frame->width, target_frame->height, QImage::Format_RGBA8888); + + // create QPainter process + QPainter p(&img); + + // set font color to white + p.setPen(Qt::white); + + // set font size relative to frame size (divided by 12) + QFont overlay_font = p.font(); + overlay_font.setPixelSize(target_frame->height/12); + p.setFont(overlay_font); + + // generate overlay text + QString proxy_overlay_text = QCoreApplication::translate("Playback", "Generating Proxy: %1%").arg(proxy_generator.get_proxy_progress(c->media->to_footage())); + + int text_height = p.fontMetrics().descent() + p.fontMetrics().height(); + + // draw semi-transparent black background + p.fillRect(QRect(0, + target_frame->height-text_height, + p.fontMetrics().width(proxy_overlay_text), + text_height), + QColor(0, 0, 0, 128) + ); + + + // draw text + p.drawText(0, + target_frame->height-p.fontMetrics().descent(), + proxy_overlay_text); + } for (int i=0;ieffects.size();i++) { Effect* e = c->effects.at(i); if (e->enable_image && e->is_enabled()) { if (data_buffer_1 == target_frame->data[0]) { - frame_size = size_t(target_frame->linesize[0])*size_t(target_frame->height); - data_buffer_1 = new uint8_t[frame_size]; data_buffer_2 = new uint8_t[frame_size]; diff --git a/project/footage.cpp b/project/footage.cpp index c5cdc94e9..82ecc5915 100644 --- a/project/footage.cpp +++ b/project/footage.cpp @@ -18,7 +18,8 @@ Footage::Footage() : in(0), out(0), speed(1.0), - alpha_is_premultiplied(false) + alpha_is_premultiplied(false), + proxy(false) { ready_lock.lock(); } diff --git a/project/footage.h b/project/footage.h index 56ae6cace..6e5219078 100644 --- a/project/footage.h +++ b/project/footage.h @@ -56,6 +56,10 @@ struct Footage { double speed; bool alpha_is_premultiplied; + // proxy config + bool proxy; + QString proxy_path; + PreviewGenerator* preview_gen; QMutex ready_lock; diff --git a/project/sourcescommon.cpp b/project/sourcescommon.cpp index 957eb5b7a..b856fbcd1 100644 --- a/project/sourcescommon.cpp +++ b/project/sourcescommon.cpp @@ -3,13 +3,17 @@ #include "panels/panels.h" #include "project/media.h" #include "project/undo.h" +#include "playback/playback.h" #include "panels/timeline.h" #include "panels/project.h" #include "project/footage.h" #include "panels/viewer.h" #include "project/projectfilter.h" +#include "project/sequence.h" #include "io/config.h" #include "dialogs/proxydialog.h" +#include "ui/viewerwidget.h" +#include "io/proxygenerator.h" #include "mainwindow.h" #include @@ -19,6 +23,8 @@ #include #include +#include + SourcesCommon::SourcesCommon(Project* parent) : editing_item(nullptr), project_parent(parent) @@ -76,11 +82,11 @@ void SourcesCommon::show_context_menu(QWidget* parent, const QModelIndexList& it connect(show_sequences, SIGNAL(triggered(bool)), panel_project->sorter, SLOT(set_show_sequences(bool))); if (items.size() > 0) { - Media* m = project_parent->item_to_media(items.at(0)); - if (items.size() == 1) { + Media* first_media = project_parent->item_to_media(items.at(0)); + // replace footage - int type = m->get_type(); + int type = first_media->get_type(); if (type == MEDIA_TYPE_FOOTAGE) { QAction* replace_action = menu.addAction(tr("Replace/Relink Media")); QObject::connect(replace_action, SIGNAL(triggered(bool)), project_parent, SLOT(replace_selected_file())); @@ -100,11 +106,13 @@ void SourcesCommon::show_context_menu(QWidget* parent, const QModelIndexList& it } } - // duplicate item + // analyze selected footage types bool all_sequences = true; bool all_footage = true; + cached_selected_footage.clear(); for (int i=0;iitem_to_media(items.at(i)); if (m->get_type() != MEDIA_TYPE_SEQUENCE) { all_sequences = false; } @@ -132,9 +140,52 @@ void SourcesCommon::show_context_menu(QWidget* parent, const QModelIndexList& it QObject::connect(delete_footage_from_sequences, SIGNAL(triggered(bool)), project_parent, SLOT(delete_clips_using_selected_media())); QMenu* proxies = menu.addMenu(tr("Proxy")); - proxies->addAction(tr("Create Proxy"), this, SLOT(open_create_proxy_dialog())); -// proxies->addAction(tr("Modify Proxy")); -// proxies->addAction(tr("Restore Original")); + + // special case if one footage item is selected and its proxy is currently being generated + if (cached_selected_footage.size() == 1 + && cached_selected_footage.at(0)->proxy + && cached_selected_footage.at(0)->proxy_path.isEmpty()) { + QAction* action = proxies->addAction(tr("Generating proxy: %1% complete").arg(proxy_generator.get_proxy_progress(cached_selected_footage.at(0)))); + action->setEnabled(false); + } else { + // determine whether any selected footage has or doesn't have proxies + bool footage_without_proxies_exists = false; + bool footage_with_proxies_exists = false; + + for (int i=0;iproxy) { + footage_with_proxies_exists = true; + } else { + footage_without_proxies_exists = true; + } + } + + // if footage was selected WITHOUT proxies + if (footage_without_proxies_exists) { + QString create_proxy_text; + + if (footage_with_proxies_exists) { + // some of the footage already has proxies, so we use a different string + create_proxy_text = tr("Create/Modify Proxy"); + } else { + // none of the footage has proxies + create_proxy_text = tr("Create Proxy"); + } + + proxies->addAction(create_proxy_text, this, SLOT(open_create_proxy_dialog())); + } + + // if footage was selected WITH proxies + if (footage_with_proxies_exists) { + + if (!footage_without_proxies_exists) { + // if all the footage has proxies, we didn't make a "Create/Modify" above, so we create one here (but only "modify") + proxies->addAction(tr("Modify Proxy"), this, SLOT(open_create_proxy_dialog())); + } + + proxies->addAction(tr("Restore Original"), this, SLOT(clear_proxies_from_selected())); + } + } } // delete media @@ -308,3 +359,38 @@ void SourcesCommon::open_create_proxy_dialog() { ProxyDialog pd(mainWindow, cached_selected_footage); pd.exec(); } + +void SourcesCommon::clear_proxies_from_selected() { + QList delete_list; + + for (int i=0;iproxy && !f->proxy_path.isEmpty()) { + if (QFileInfo::exists(f->proxy_path)) { + if (QMessageBox::question(mainWindow, + tr("Delete proxy"), + tr("Would you like to delete the proxy file \"%1\" as well?").arg(f->proxy_path), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + delete_list.append(f->proxy_path); + } + } + } + + f->proxy = false; + f->proxy_path.clear(); + } + + if (sequence != nullptr) { + // close all clips so we can delete any proxies requested to be deleted + closeActiveClips(sequence); + + // delete proxies requested to be deleted + for (int i=0;iviewer_widget->frame_update(); + } +} diff --git a/project/sourcescommon.h b/project/sourcescommon.h index 3c15e5ae2..1eb816129 100644 --- a/project/sourcescommon.h +++ b/project/sourcescommon.h @@ -30,7 +30,10 @@ private slots: void reveal_in_browser(); void rename_interval(); void item_renamed(Media *item); + + // proxy functions void open_create_proxy_dialog(); + void clear_proxies_from_selected(); private: Media* editing_item; QModelIndex editing_index; diff --git a/ui/renderthread.cpp b/ui/renderthread.cpp index a8ef4fee5..b85a3d349 100644 --- a/ui/renderthread.cpp +++ b/ui/renderthread.cpp @@ -58,7 +58,6 @@ void RenderThread::run() { ctx->functions()->glGenFramebuffers(1, &back_buffer_2); } - // gen texture if (front_texture == 0 || tex_width != seq->width || tex_height != seq->height) { // cache texture size From 9861174c6335694c7b008a02da5e6480ec464457 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 1 Feb 2019 19:53:52 +1100 Subject: [PATCH 3/4] temporarily removed unusable proxy codec options --- dialogs/proxydialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dialogs/proxydialog.cpp b/dialogs/proxydialog.cpp index 5d7b47469..f190109d8 100644 --- a/dialogs/proxydialog.cpp +++ b/dialogs/proxydialog.cpp @@ -41,10 +41,10 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) : format_combobox = new QComboBox(this); format_combobox->addItem(tr("ProRes HQ")); - format_combobox->addItem(tr("ProRes SQ")); - format_combobox->addItem(tr("ProRes LT")); - format_combobox->addItem(tr("DNxHD")); - format_combobox->addItem(tr("H.264")); +// format_combobox->addItem(tr("ProRes SQ")); +// format_combobox->addItem(tr("ProRes LT")); +// format_combobox->addItem(tr("DNxHD")); +// format_combobox->addItem(tr("H.264")); layout->addWidget(format_combobox, 1, 1); // set the location to place the proxies From d39d57c1ff3ebddc6e3840ae3ada3a033366e8a5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 2 Feb 2019 14:54:26 +1100 Subject: [PATCH 4/4] change modified status on proxy change --- dialogs/proxydialog.cpp | 2 ++ project/sourcescommon.cpp | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dialogs/proxydialog.cpp b/dialogs/proxydialog.cpp index f190109d8..fd4407027 100644 --- a/dialogs/proxydialog.cpp +++ b/dialogs/proxydialog.cpp @@ -114,6 +114,8 @@ void ProxyDialog::accept() { proxy_generator.queue(info_list.at(i)); } + mainWindow->setWindowModified(true); + QDialog::accept(); } diff --git a/project/sourcescommon.cpp b/project/sourcescommon.cpp index b856fbcd1..0c7f55fcd 100644 --- a/project/sourcescommon.cpp +++ b/project/sourcescommon.cpp @@ -384,13 +384,17 @@ void SourcesCommon::clear_proxies_from_selected() { if (sequence != nullptr) { // close all clips so we can delete any proxies requested to be deleted closeActiveClips(sequence); + } - // delete proxies requested to be deleted - for (int i=0;iviewer_widget->frame_update(); } + + mainWindow->setWindowModified(true); }