backend transcoding done for proxies

This commit is contained in:
itsmattkc
2019-01-31 23:01:28 +11:00
parent c8aec606d0
commit c779ec43e7
13 changed files with 493 additions and 35 deletions
+1 -1
View File
@@ -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);
+59 -3
View File
@@ -5,8 +5,17 @@
#include <QDialogButtonBox>
#include <QComboBox>
#include <QFileDialog>
#include <QMessageBox>
ProxyDialog::ProxyDialog(QWidget *parent, const QVector<Footage *> &footage) : QDialog(parent) {
#include <QDebug>
#include "io/proxygenerator.h"
#include "project/footage.h"
ProxyDialog::ProxyDialog(QWidget *parent, const QVector<Footage *> &footage) :
QDialog(parent),
selected_footage(footage)
{
// set dialog title
setWindowTitle(tr("Create Proxy"));
@@ -19,7 +28,7 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector<Footage *> &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 *> &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 *> &footage) : Q
connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
}
void ProxyDialog::accept() {
QVector<ProxyInfo> info_list;
for (int i=0;i<selected_footage.size();i++) {
// loop through selected footage and send info to the proxy queue
ProxyInfo info;
// fill info struct based on user input
info.footage = selected_footage.at(i);
info.codec_type = 0;
info.size_multiplier = size_combobox->currentData().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<info_list.size();i++) {
proxy_generator.queue(info_list.at(i));
}
QDialog::accept();
}
void ProxyDialog::location_changed(int i) {
custom_location.clear();
if (i == 1) {
+12 -2
View File
@@ -11,19 +11,29 @@ class ProxyDialog : public QDialog {
Q_OBJECT
public:
ProxyDialog(QWidget* parent, const QVector<Footage*>& 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<Footage*> selected_footage;
private slots:
// triggered when the user changes the index in the location combobox
void location_changed(int i);
};
+2 -2
View File
@@ -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<enum AVCodecID>(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<AVCodecID>(video_codec);
vcodec_ctx->codec_id = static_cast<enum AVCodecID>(video_codec);
vcodec_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
vcodec_ctx->width = video_width;
vcodec_ctx->height = video_height;
+9
View File
@@ -3,6 +3,9 @@
#include <QStandardPaths>
#include <QFileInfo>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include "debug.h"
QString real_app_dir;
@@ -41,3 +44,9 @@ QList<QString> 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();
}
+3
View File
@@ -8,4 +8,7 @@ QString get_data_path();
QString get_config_path();
QList<QString> get_effects_paths();
// generate hash algorithm used to uniquely identify files
QString get_file_hash(const QString& filename);
#endif // PATH_H
+5 -20
View File
@@ -13,10 +13,8 @@
#include <QtMath>
#include <QTreeWidgetItem>
#include <QSemaphore>
#include <QCryptographicHash>
#include <QFile>
#include <QDir>
#include <QDateTime>
#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();
+340
View File
@@ -0,0 +1,340 @@
#include "proxygenerator.h"
#include "project/footage.h"
#include "io/path.h"
#include <QDir>
#include <QFileInfo>
#include <QtMath>
#include <QDebug>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
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<AVCodecContext*> input_streams;
input_streams.resize(input_fmt_ctx->nb_streams);
input_streams.fill(nullptr);
// create array of output encoders
QVector<AVCodecContext*> output_streams;
output_streams.resize(input_fmt_ctx->nb_streams);
output_streams.fill(nullptr);
// create array of swscale contexts for pixel format conversion
QVector<SwsContext*> 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;i<int(input_fmt_ctx->nb_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<enum AVPixelFormat>(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;i<int(input_fmt_ctx->nb_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;
+35
View File
@@ -0,0 +1,35 @@
#ifndef PROXYGENERATOR_H
#define PROXYGENERATOR_H
#include <QThread>
#include <QVector>
#include <QMutex>
#include <QWaitCondition>
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<ProxyInfo> proxy_queue;
QWaitCondition waitCond;
QMutex mutex;
bool cancelled;
};
// proxy generator is a global omnipotent entity
extern ProxyGenerator proxy_generator;
#endif // PROXYGENERATOR_H
+11 -1
View File
@@ -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;
+4 -2
View File
@@ -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 +=
+6 -4
View File
@@ -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;i<items.size();i++) {
if (m->get_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<Footage*> 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();
}
+6
View File
@@ -3,6 +3,7 @@
#include <QModelIndexList>
#include <QTimer>
#include <QVector>
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<Footage*> cached_selected_footage;
};
#endif // SOURCESCOMMON_H