@@ -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);
|
||||
|
||||
|
||||
+68
-7
@@ -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,12 +39,12 @@ 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"));
|
||||
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
|
||||
@@ -58,6 +67,58 @@ 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++) {
|
||||
info_list.at(i).footage->proxy = true;
|
||||
info_list.at(i).footage->proxy_path.clear();
|
||||
|
||||
proxy_generator.queue(info_list.at(i));
|
||||
}
|
||||
|
||||
mainWindow->setWindowModified(true);
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void ProxyDialog::location_changed(int i) {
|
||||
custom_location.clear();
|
||||
if (i == 1) {
|
||||
|
||||
+12
-2
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -13,10 +13,8 @@
|
||||
#include <QtMath>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QSemaphore>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
@@ -65,13 +63,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('%')) {
|
||||
@@ -82,16 +74,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;
|
||||
@@ -238,7 +224,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;
|
||||
@@ -455,10 +443,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();
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
#include "proxygenerator.h"
|
||||
|
||||
#include "project/footage.h"
|
||||
#include "io/path.h"
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QtMath>
|
||||
#include <QStatusBar>
|
||||
|
||||
#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 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);
|
||||
|
||||
// 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 (!skip) {
|
||||
|
||||
// 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);
|
||||
|
||||
// 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) && !skip);
|
||||
|
||||
// error/eof handling - cancel while loop
|
||||
if (read_ret < 0 || skip) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// return value for packet receiving
|
||||
int recret;
|
||||
|
||||
// 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
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(".");
|
||||
|
||||
// set skip to false
|
||||
skip = false;
|
||||
|
||||
// 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) {
|
||||
// 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.size();i++) {
|
||||
if (proxy_queue.at(i).footage == info.footage) {
|
||||
// found a duplicate, assume the one we're queuing now overrides and delete it
|
||||
proxy_queue.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
skip = true;
|
||||
|
||||
// if signal is sleeping, wake it to cancel correctly
|
||||
waitCond.wakeAll();
|
||||
|
||||
// wait for thread to finish
|
||||
wait();
|
||||
}
|
||||
|
||||
double ProxyGenerator::get_proxy_progress(Footage *f) {
|
||||
if (proxy_queue.first().footage == f) {
|
||||
return current_progress;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// proxy generator is a global omnipotent entity
|
||||
ProxyGenerator proxy_generator;
|
||||
@@ -0,0 +1,50 @@
|
||||
#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();
|
||||
double get_proxy_progress(Footage* f);
|
||||
private:
|
||||
// queue of footage to process proxies for
|
||||
QVector<ProxyInfo> 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
|
||||
extern ProxyGenerator proxy_generator;
|
||||
|
||||
#endif // PROXYGENERATOR_H
|
||||
+11
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "io/config.h"
|
||||
#include "io/path.h"
|
||||
#include "io/proxygenerator.h"
|
||||
|
||||
#include "project/footage.h"
|
||||
#include "project/sequence.h"
|
||||
@@ -219,6 +220,7 @@ MainWindow::MainWindow(QWidget *parent, const QString &an) :
|
||||
statusBar->showMessage(tr("Welcome to %1").arg(appName));
|
||||
setStatusBar(statusBar);
|
||||
|
||||
// populate menu bars
|
||||
setup_menus();
|
||||
|
||||
if (!data_dir.isEmpty()) {
|
||||
@@ -235,9 +237,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() {
|
||||
@@ -1002,6 +1009,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);
|
||||
@@ -1051,8 +1061,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;
|
||||
|
||||
@@ -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 +=
|
||||
|
||||
|
||||
@@ -966,6 +966,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;j<f->video_tracks.size();j++) {
|
||||
const FootageStream& ms = f->video_tracks.at(j);
|
||||
stream.writeStartElement("video");
|
||||
|
||||
+13
-1
@@ -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);
|
||||
|
||||
|
||||
+47
-3
@@ -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 <QOpenGLTexture>
|
||||
#include <QOpenGLPixelTransferOptions>
|
||||
#include <QOpenGLFramebufferObject>
|
||||
#include <QPainter>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
//#define GCF_DEBUG
|
||||
@@ -270,14 +273,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;i<c->effects.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];
|
||||
|
||||
|
||||
+2
-1
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+103
-11
@@ -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 <QProcess>
|
||||
@@ -19,6 +23,8 @@
|
||||
#include <QMessageBox>
|
||||
#include <QDesktopServices>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
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,14 +106,19 @@ 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;i<items.size();i++) {
|
||||
Media* m = project_parent->item_to_media(items.at(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;
|
||||
}
|
||||
}
|
||||
@@ -129,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;i<cached_selected_footage.size();i++) {
|
||||
if (cached_selected_footage.at(i)->proxy) {
|
||||
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
|
||||
@@ -301,8 +355,46 @@ 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();
|
||||
}
|
||||
|
||||
void SourcesCommon::clear_proxies_from_selected() {
|
||||
QList<QString> delete_list;
|
||||
|
||||
for (int i=0;i<cached_selected_footage.size();i++) {
|
||||
Footage* f = cached_selected_footage.at(i);
|
||||
|
||||
if (f->proxy && !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;i<delete_list.size();i++) {
|
||||
QFile::remove(delete_list.at(i));
|
||||
}
|
||||
|
||||
if (sequence != nullptr) {
|
||||
// update viewer (will re-open active clips with original media)
|
||||
panel_sequence_viewer->viewer_widget->frame_update();
|
||||
}
|
||||
|
||||
mainWindow->setWindowModified(true);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -27,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;
|
||||
@@ -35,6 +41,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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user