giant rendering overhaul and many small fixes
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio.h"
|
||||
|
||||
#include "oliveglobal.h"
|
||||
|
||||
#include "project/sequence.h"
|
||||
|
||||
#include "panels/panels.h"
|
||||
|
||||
#include "io/config.h"
|
||||
#include "ui/audiomonitor.h"
|
||||
#include "rendering/renderfunctions.h"
|
||||
#include "debug.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QAudioOutput>
|
||||
#include <QAudioInput>
|
||||
#include <QtMath>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QComboBox>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
}
|
||||
|
||||
QAudioOutput* audio_output;
|
||||
QIODevice* audio_io_device;
|
||||
bool audio_device_set = false;
|
||||
bool audio_scrub = false;
|
||||
QMutex audio_write_lock;
|
||||
QAudioInput* audio_input = nullptr;
|
||||
QFile output_recording;
|
||||
bool audio_rendering = false;
|
||||
bool recording = false;
|
||||
|
||||
qint8 audio_ibuffer[audio_ibuffer_size];
|
||||
qint64 audio_ibuffer_read = 0;
|
||||
long audio_ibuffer_frame = 0;
|
||||
double audio_ibuffer_timecode = 0;
|
||||
|
||||
AudioSenderThread* audio_thread = nullptr;
|
||||
|
||||
bool is_audio_device_set() {
|
||||
return audio_device_set;
|
||||
}
|
||||
|
||||
QAudioDeviceInfo get_audio_device(QAudio::Mode mode) {
|
||||
QList<QAudioDeviceInfo> devs = QAudioDeviceInfo::availableDevices(mode);
|
||||
|
||||
// try to retrieve preferred device from config
|
||||
QString preferred_device = (mode == QAudio::AudioOutput) ? olive::CurrentConfig.preferred_audio_output : olive::CurrentConfig.preferred_audio_input;
|
||||
if (!preferred_device.isEmpty()) {
|
||||
for (int i=0;i<devs.size();i++) {
|
||||
// try to match available devices with preferred device
|
||||
if (devs.at(i).deviceName() == preferred_device) {
|
||||
return devs.at(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no preferred output is set, try to get the default device
|
||||
QAudioDeviceInfo default_device = (mode == QAudio::AudioOutput) ? QAudioDeviceInfo::defaultOutputDevice() : QAudioDeviceInfo::defaultInputDevice();
|
||||
if (!default_device.isNull()) {
|
||||
return default_device;
|
||||
}
|
||||
|
||||
// if no default output could be retrieved, just use the first in the list
|
||||
if (devs.size() > 0) {
|
||||
return devs.at(0);
|
||||
}
|
||||
|
||||
// couldn't find any audio devices, return null device
|
||||
return QAudioDeviceInfo();
|
||||
}
|
||||
|
||||
void init_audio() {
|
||||
stop_audio();
|
||||
|
||||
QAudioFormat audio_format;
|
||||
audio_format.setSampleRate(olive::CurrentConfig.audio_rate);
|
||||
audio_format.setChannelCount(2);
|
||||
audio_format.setSampleSize(16);
|
||||
audio_format.setCodec("audio/pcm");
|
||||
audio_format.setByteOrder(QAudioFormat::LittleEndian);
|
||||
audio_format.setSampleType(QAudioFormat::SignedInt);
|
||||
|
||||
QAudioDeviceInfo info = get_audio_device(QAudio::AudioOutput);
|
||||
|
||||
// see if desired format can be used by the device, use nearest if not
|
||||
if (!info.isFormatSupported(audio_format)) {
|
||||
qWarning() << "Audio format is not supported by backend, using nearest";
|
||||
audio_format = info.nearestFormat(audio_format);
|
||||
}
|
||||
|
||||
audio_output = new QAudioOutput(info, audio_format);
|
||||
audio_output->moveToThread(QApplication::instance()->thread());
|
||||
audio_output->setNotifyInterval(5);
|
||||
|
||||
// connect
|
||||
audio_io_device = audio_output->start();
|
||||
if (audio_io_device == nullptr) {
|
||||
qWarning() << "Received nullptr audio device. No compatible audio output was found.";
|
||||
} else {
|
||||
audio_device_set = true;
|
||||
|
||||
// start sender thread
|
||||
audio_thread = new AudioSenderThread();
|
||||
QObject::connect(audio_output, SIGNAL(notify()), audio_thread, SLOT(notifyReceiver()));
|
||||
audio_thread->start(QThread::TimeCriticalPriority);
|
||||
|
||||
clear_audio_ibuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void stop_audio() {
|
||||
if (audio_device_set) {
|
||||
audio_thread->stop();
|
||||
|
||||
audio_output->stop();
|
||||
delete audio_output;
|
||||
audio_device_set = false;
|
||||
}
|
||||
}
|
||||
|
||||
void clear_audio_ibuffer() {
|
||||
if (audio_thread != nullptr) audio_thread->lock.lock();
|
||||
audio_write_lock.lock();
|
||||
memset(audio_ibuffer, 0, audio_ibuffer_size);
|
||||
audio_ibuffer_read = 0;
|
||||
audio_write_lock.unlock();
|
||||
if (audio_thread != nullptr) audio_thread->lock.unlock();
|
||||
}
|
||||
|
||||
int current_audio_freq() {
|
||||
return audio_rendering ? olive::ActiveSequence->audio_frequency : audio_output->format().sampleRate();
|
||||
}
|
||||
|
||||
qint64 get_buffer_offset_from_frame(double framerate, long frame) {
|
||||
if (frame >= audio_ibuffer_frame) {
|
||||
int multiplier = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)*av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
|
||||
return qFloor((double(frame - audio_ibuffer_frame)/framerate)*current_audio_freq())*multiplier;
|
||||
} else {
|
||||
qWarning() << "Invalid values passed to get_buffer_offset_from_frame" << frame << "<" << audio_ibuffer_frame;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
AudioSenderThread::AudioSenderThread() : close(false) {
|
||||
connect(this, SIGNAL(finished()), this, SLOT(deleteLater()));
|
||||
}
|
||||
|
||||
void AudioSenderThread::stop() {
|
||||
close = true;
|
||||
cond.wakeAll();
|
||||
wait();
|
||||
}
|
||||
|
||||
void AudioSenderThread::notifyReceiver() {
|
||||
cond.wakeAll();
|
||||
}
|
||||
|
||||
void AudioSenderThread::run() {
|
||||
// start data loop
|
||||
send_audio_to_output(0, audio_ibuffer_size);
|
||||
|
||||
lock.lock();
|
||||
while (true) {
|
||||
cond.wait(&lock);
|
||||
if (close) {
|
||||
break;
|
||||
} else if (panel_sequence_viewer->playing || panel_footage_viewer->playing || audio_scrub) {
|
||||
int written_bytes = 0;
|
||||
|
||||
int adjusted_read_index = audio_ibuffer_read%audio_ibuffer_size;
|
||||
int max_write = audio_ibuffer_size - adjusted_read_index;
|
||||
int actual_write = send_audio_to_output(adjusted_read_index, max_write);
|
||||
written_bytes += actual_write;
|
||||
if (actual_write == max_write) {
|
||||
// got all the bytes, write again
|
||||
written_bytes += send_audio_to_output(0, audio_ibuffer_size);
|
||||
}
|
||||
|
||||
audio_scrub = false;
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
int AudioSenderThread::send_audio_to_output(qint64 offset, int max) {
|
||||
// send audio to device
|
||||
qint64 actual_write = audio_io_device->write(reinterpret_cast<const char*>(audio_ibuffer)+offset, max);
|
||||
|
||||
qint64 audio_ibuffer_limit = audio_ibuffer_read + actual_write;
|
||||
|
||||
if (actual_write > 0) {
|
||||
// average values and send to audio monitor
|
||||
int channels = audio_output->format().channelCount();
|
||||
qint64 lim = offset + actual_write;
|
||||
QVector<double> averages;
|
||||
averages.resize(channels);
|
||||
averages.fill(0);
|
||||
|
||||
int counter = 0;
|
||||
qint16 sample;
|
||||
for (qint64 i=offset;i<lim;i+=2) {
|
||||
sample = qint16(((audio_ibuffer[i+1] & 0xFF) << 8) | (audio_ibuffer[i] & 0xFF));
|
||||
averages[counter] = qMax((double(qAbs(sample))/32768.0), averages[counter]);
|
||||
counter = (counter+1)%channels;
|
||||
}
|
||||
for (int i=0;i<channels;i++) {
|
||||
averages[i] = log_volume(1.0-(averages[i]));
|
||||
}
|
||||
|
||||
panel_timeline->audio_monitor->set_value(averages);
|
||||
}
|
||||
|
||||
memset(audio_ibuffer+offset, 0, actual_write);
|
||||
|
||||
audio_ibuffer_read = audio_ibuffer_limit;
|
||||
|
||||
return actual_write;
|
||||
}
|
||||
|
||||
double log_volume(double linear) {
|
||||
// expects a value between 0 and 1 (or more if amplifying)
|
||||
return (qExp(linear)-1)/(M_E-1);
|
||||
}
|
||||
|
||||
void int32_to_char_array(qint32 i, char* array) {
|
||||
memcpy(array, &i, 4);
|
||||
}
|
||||
|
||||
void write_wave_header(QFile& f, const QAudioFormat& format) {
|
||||
qint32 int32bit;
|
||||
char arr[4];
|
||||
|
||||
// 4 byte riff header
|
||||
f.write("RIFF");
|
||||
|
||||
// 4 byte file size, filled in later
|
||||
for (int i=0;i<4;i++) f.putChar(0);
|
||||
|
||||
// 4 byte file type header + 4 byte format chunk marker
|
||||
f.write("WAVEfmt");
|
||||
f.putChar(0x20);
|
||||
|
||||
// 4 byte length of the above format data (always 16 bytes)
|
||||
f.putChar(16);
|
||||
for (int i=0;i<3;i++) f.putChar(0);
|
||||
|
||||
// 2 byte type format (1 is PCM)
|
||||
f.putChar(1);
|
||||
f.putChar(0);
|
||||
|
||||
// 2 byte channel count
|
||||
int32bit = format.channelCount();
|
||||
int32_to_char_array(int32bit, arr);
|
||||
f.write(arr, 2);
|
||||
|
||||
// 4 byte integer for sample rate
|
||||
int32bit = format.sampleRate();
|
||||
int32_to_char_array(int32bit, arr);
|
||||
f.write(arr, 4);
|
||||
|
||||
// 4 byte integer for bytes per second
|
||||
int32bit = (format.sampleRate() * format.sampleSize() * format.channelCount()) / 8;
|
||||
int32_to_char_array(int32bit, arr);
|
||||
f.write(arr, 4);
|
||||
|
||||
// 2 byte integer for bytes per sample per channel
|
||||
int32bit = (format.sampleSize() * format.channelCount()) / 8;
|
||||
int32_to_char_array(int32bit, arr);
|
||||
f.write(arr, 2);
|
||||
|
||||
// 2 byte integer for bits per sample (16)
|
||||
int32bit = format.sampleSize();
|
||||
int32_to_char_array(int32bit, arr);
|
||||
f.write(arr, 2);
|
||||
|
||||
// data chunk header
|
||||
f.write("data");
|
||||
|
||||
// 4 byte integer for data chunk size (filled in later)?
|
||||
for (int i=0;i<4;i++) f.putChar(0);
|
||||
}
|
||||
|
||||
void write_wave_trailer(QFile& f) {
|
||||
char arr[4];
|
||||
|
||||
f.seek(4);
|
||||
|
||||
// 4 bytes for total file size - 8 bytes
|
||||
qint32 file_size = qint32(f.size()) - 8;
|
||||
int32_to_char_array(file_size, arr);
|
||||
f.write(arr, 4);
|
||||
|
||||
f.seek(40);
|
||||
|
||||
// 4 bytes for data chunk size (file size - header)
|
||||
file_size = qint32(f.size()) - 44;
|
||||
int32_to_char_array(file_size, arr);
|
||||
f.write(arr, 4);
|
||||
}
|
||||
|
||||
bool start_recording() {
|
||||
if (olive::ActiveSequence == nullptr) {
|
||||
qCritical() << "No active sequence to record into";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString audio_path = QCoreApplication::translate("Audio", "%1 Audio").arg(olive::ActiveProjectFilename);
|
||||
QDir audio_dir(audio_path);
|
||||
if (!audio_dir.exists() && !audio_dir.mkpath(".")) {
|
||||
qCritical() << "Failed to create audio directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString audio_file_path;
|
||||
int file_number = 0;
|
||||
do {
|
||||
file_number++;
|
||||
|
||||
QString audio_filename = QString("%1.wav").arg(
|
||||
QCoreApplication::translate("Audio", "Recording %1").arg(QString::number(file_number))
|
||||
);
|
||||
|
||||
audio_file_path = audio_dir.filePath(audio_filename);
|
||||
} while (QFile(audio_file_path).exists());
|
||||
|
||||
output_recording.setFileName(audio_file_path);
|
||||
if (!output_recording.open(QFile::WriteOnly)) {
|
||||
qCritical() << "Failed to open output file. Does Olive have permission to write to this directory?";
|
||||
return false;
|
||||
}
|
||||
|
||||
QAudioFormat audio_format = audio_output->format();
|
||||
if (olive::CurrentConfig.recording_mode != audio_format.channelCount()) {
|
||||
audio_format.setChannelCount(olive::CurrentConfig.recording_mode);
|
||||
}
|
||||
|
||||
QAudioDeviceInfo info = get_audio_device(QAudio::AudioInput);
|
||||
|
||||
if (!info.isFormatSupported(audio_format)) {
|
||||
qWarning() << "Default format not supported, using nearest";
|
||||
audio_format = info.nearestFormat(audio_format);
|
||||
}
|
||||
write_wave_header(output_recording, audio_format);
|
||||
audio_input = new QAudioInput(info, audio_format);
|
||||
audio_input->start(&output_recording);
|
||||
recording = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop_recording() {
|
||||
if (recording) {
|
||||
audio_input->stop();
|
||||
|
||||
write_wave_trailer(output_recording);
|
||||
|
||||
output_recording.close();
|
||||
|
||||
delete audio_input;
|
||||
audio_input = nullptr;
|
||||
recording = false;
|
||||
}
|
||||
}
|
||||
|
||||
QString get_recorded_audio_filename() {
|
||||
return output_recording.fileName();
|
||||
}
|
||||
|
||||
void combobox_audio_sample_rates(QComboBox *combobox) {
|
||||
combobox->addItem("22050 Hz", 22050);
|
||||
combobox->addItem("24000 Hz", 24000);
|
||||
combobox->addItem("32000 Hz", 32000);
|
||||
combobox->addItem("44100 Hz", 44100);
|
||||
combobox->addItem("48000 Hz", 48000);
|
||||
combobox->addItem("88200 Hz", 88200);
|
||||
combobox->addItem("96000 Hz", 96000);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef AUDIO_H
|
||||
#define AUDIO_H
|
||||
|
||||
#include <QVector>
|
||||
#include <QThread>
|
||||
#include <QWaitCondition>
|
||||
#include <QMutex>
|
||||
#include <QIODevice>
|
||||
#include <QAudioOutput>
|
||||
#include <QComboBox>
|
||||
|
||||
#include "project/sequence.h"
|
||||
|
||||
class AudioSenderThread : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioSenderThread();
|
||||
void run();
|
||||
void stop();
|
||||
QWaitCondition cond;
|
||||
bool close;
|
||||
QMutex lock;
|
||||
public slots:
|
||||
void notifyReceiver();
|
||||
private:
|
||||
QVector<qint16> samples;
|
||||
int send_audio_to_output(qint64 offset, int max);
|
||||
};
|
||||
|
||||
double log_volume(double linear);
|
||||
|
||||
extern QAudioOutput* audio_output;
|
||||
extern QIODevice* audio_io_device;
|
||||
extern AudioSenderThread* audio_thread;
|
||||
extern QMutex audio_write_lock;
|
||||
|
||||
#define audio_ibuffer_size 192000
|
||||
extern qint8 audio_ibuffer[audio_ibuffer_size];
|
||||
extern qint64 audio_ibuffer_read;
|
||||
extern long audio_ibuffer_frame;
|
||||
extern double audio_ibuffer_timecode;
|
||||
extern bool audio_scrub;
|
||||
extern bool recording;
|
||||
extern bool audio_rendering;
|
||||
void clear_audio_ibuffer();
|
||||
|
||||
int current_audio_freq();
|
||||
|
||||
bool is_audio_device_set();
|
||||
|
||||
void init_audio();
|
||||
void stop_audio();
|
||||
qint64 get_buffer_offset_from_frame(double framerate, long frame);
|
||||
|
||||
bool start_recording();
|
||||
void stop_recording();
|
||||
QString get_recorded_audio_filename();
|
||||
|
||||
void combobox_audio_sample_rates(QComboBox* combobox);
|
||||
|
||||
#endif // AUDIO_H
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,573 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef CACHER_H
|
||||
#define CACHER_H
|
||||
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libavfilter/avfilter.h>
|
||||
#include <libavfilter/buffersrc.h>
|
||||
#include <libavfilter/buffersink.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
#include <memory>
|
||||
#include <QThread>
|
||||
#include <QVector>
|
||||
#include <QWaitCondition>
|
||||
#include <QMutex>
|
||||
|
||||
#include "rendering/clipqueue.h"
|
||||
|
||||
class Clip;
|
||||
using ClipPtr = std::shared_ptr<Clip>;
|
||||
|
||||
/**
|
||||
* @brief The Cacher class
|
||||
*
|
||||
* For footage clips - usually the majority of clips - decoding can be strenuous on CPU and inconsistent in timing. As
|
||||
* a result, we keep a memory cache of upcoming frames that we fill in a background thread so they can be retrieved from
|
||||
* a rendering thread later. This class is the background thread filling up a clip's frame cache (also called a "queue"
|
||||
* since video files are usually stored with frames in linear chronological order). It involves decoding routines to
|
||||
* retrieve raw frames from the file (using libavformat/libavcodec), conversion routines to conform the raw frames to
|
||||
* RGBA/S16LE for the rest of the workflow (using libavfilter/libswscale/libswresample), and memory handling routines
|
||||
* for keeping the cache within limits defined by the user (see Config::upcoming_queue_type).
|
||||
*
|
||||
* Generally the Cacher workflow starts by calling Open() which will start the thread, open a file handle, and create a
|
||||
* decoding instance. Open() is usually called directly from the parent Clip's Clip::Open() and thus expects the
|
||||
* Clip::state_change_lock to be locked. It will unlock it when it's finished opening and is ready to start caching,
|
||||
* meaning Clip::state_change_lock can be used to synchronize threads.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* **For video:**
|
||||
*
|
||||
* After the Cacher has finished opening, request a frame by calling Cache(). Cache() will tell the Cacher
|
||||
* information about the current playback state, most importantly the current place in time according to the Sequence's
|
||||
* playhead. Cache() determines whether the requested frame is already in the queue, and then signals the Cacher thread
|
||||
* to cache ahead if there's room in the queue (and also remove old frames that are no longer necessary). To retrieve
|
||||
* the requested frame, call Retrieve().
|
||||
*
|
||||
* If Cache() found the frame already in the queue, Retrieve() will return immediately with this frame. Otherwise
|
||||
* Retrieve() may block while the cacher retrieves it. Therefore it is recommended never to call Retrieve() from
|
||||
* the main thread. Retrieve() may also return `nullptr` if there was an issue, e.g. the cacher failed to retrieve
|
||||
* the frame.
|
||||
*
|
||||
* **For audio:**
|
||||
*
|
||||
* After the Cacher has finished opening, calling Cache() will handle most of the work. It will decode the audio,
|
||||
* convert to the correct sample rate and format, reverse or adjust speed if necessary, and send it to the audio
|
||||
* buffer ready to be played by the output device. It is important to continually call Cache() as it doesn't get
|
||||
* signalled when more samples are available in the audio buffer. Instead, it'll check every time it's called and
|
||||
* fill as much of the buffer as it can.
|
||||
*
|
||||
* If the user seeks, ResetAudio() must be called to signal the Cacher to interrupt the current audio stream and
|
||||
* move somewhere else before continuing.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Finally, when the Cacher/parent Clip are no longer in use, call Close() to free all memory and file handling
|
||||
* allocated for the cacher. You can choose whether to wait for Close() and all of its child processes to complete -
|
||||
* e.g. if you need to change something with the Clip or attached Footage that changes how it opens and want to be
|
||||
* thread-safe - or let the Close thread finish up on its own.
|
||||
*
|
||||
* Cacher expects to be multithreaded and all of its public functions are thread-safe.
|
||||
*/
|
||||
class Cacher : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Cacher Constructor
|
||||
*
|
||||
* Create Cacher object. The thread is not started here. To start it, call Open().
|
||||
*
|
||||
* @param c
|
||||
*/
|
||||
Cacher(ClipPtr c);
|
||||
|
||||
/**
|
||||
* @brief The main QThread loop
|
||||
*
|
||||
* Once the thread has started, all Cacher functions will be called from here until the Cacher closes at which point
|
||||
* it will close and exit gracefully.
|
||||
*/
|
||||
void run();
|
||||
|
||||
/**
|
||||
* @brief Open the cacher
|
||||
*
|
||||
* Starts the thread and all file/decode handlers. Really just sets some default values and starts the thread, which
|
||||
* will in turn call OpenWorker() at the start of its functions.
|
||||
*
|
||||
* Make sure Clip::state_change_lock is LOCKED before calling this function as the opening process will try to unlock
|
||||
* it when it's finished (leading to a crash if it's not already locked).
|
||||
*/
|
||||
void Open();
|
||||
|
||||
/**
|
||||
* @brief Request a frame to be cached
|
||||
*
|
||||
* For video, this function is part 1 of the Cache()/Retrieve() workflow. It signals the thread to start caching and
|
||||
* provides a few other details about the playback state. For optimization it'll also check the frame queue if it
|
||||
* already contains the requested frame and use it if so, potentially speeding up Retrieve() later on. Otherwise
|
||||
* it'll interrupt any currently caching operation and signal it to start again.
|
||||
* While Retrieve() will block until the correct frame is retrieved, this function will return fairly quickly (either
|
||||
* immediately if the frame was found in the queue, or once the cacher has restarted caching if not). This means
|
||||
* Cache() can be called from another thread and then that other thread can do other work while the cacher is
|
||||
* retrieving the frame, finally calling Retrieve() once the frame is absolutely necessary.
|
||||
*
|
||||
* For audio, this function will do all the work of signalling the thread to start caching and sending samples to
|
||||
* the output audio buffer. It's used in tandem with ResetAudio() when the Timeline header is changed abruptly.
|
||||
*
|
||||
* @param playhead
|
||||
*
|
||||
* The current Timeline played position in frames
|
||||
*
|
||||
* @param scrubbing
|
||||
*
|
||||
* **TRUE** if the user is currently scrubbing. **FALSE** if not.
|
||||
*
|
||||
* @param nests
|
||||
*
|
||||
* A hierarchy of nested sequences, if the playback traversed any to get to this clip.
|
||||
*
|
||||
* @param playback_speed
|
||||
*
|
||||
* The current playback speed (controlled by Shuttle Left/Stop/Right)
|
||||
*/
|
||||
void Cache(long playhead, bool scrubbing, QVector<ClipPtr>& nests, int playback_speed);
|
||||
|
||||
/**
|
||||
* @brief Retrieve frame requested by Cache()
|
||||
*
|
||||
* Part 2 of the Cache()/Retrieve() workflow, only used for video. Whichever frame was requested by Cache(), this
|
||||
* function will try to retrieve it. In most cases, this function will be pretty quick as the frame will be available
|
||||
* immediately from Cache()'s optimization or the cacher thread will be close to retrieving the correct frame anyway.
|
||||
* However it does block for however long it takes to retrieve the correct frame (if the cacher is running) so it's
|
||||
* not recommended to call this from any main/GUI thread.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The frame requested by Cache(), or `nullptr` if there was an error (e.g. the cacher wasn't running and no frame was
|
||||
* available).
|
||||
*/
|
||||
AVFrame* Retrieve();
|
||||
|
||||
/**
|
||||
* @brief Close the cacher and free any allocated memory
|
||||
*
|
||||
* When the Cacher thread is no longer needed, Close() should be called in order to free system resources. This will
|
||||
* signal the thread to exit gracefully, but will not delete the thread object since the cacher may need to be
|
||||
* re-opened later by Open().
|
||||
*
|
||||
* @param wait_for_finish
|
||||
*
|
||||
* **TRUE** if this function should block the calling thread until the Clip has finished closing. Often necessary if
|
||||
* the Clip is being closed specifically to make changes to it.
|
||||
*/
|
||||
void Close(bool wait_for_finish);
|
||||
|
||||
/**
|
||||
* @brief Interrupt and reset audio state
|
||||
*
|
||||
* Used in tandem with Cache(), only for audio clips. Cache() will decode and send audio continually as it's
|
||||
* repeatedly called. If the audio stream needs to be interrupted and moved somewhere else for any reason
|
||||
* (e.g. the user seeked somewhere else), then it's necessary to call ResetAudio() to signal the cacher to
|
||||
* seek to the next place indicated by Cache().
|
||||
*/
|
||||
void ResetAudio();
|
||||
|
||||
/**
|
||||
* @brief Retrieve current media width
|
||||
*
|
||||
* In some situations, the actual media we're using may be a different resolution to how we're treating it (e.g.
|
||||
* lower resolution proxies). While most functions will happily treat the media as its original resolution, some
|
||||
* processes will need the absolute resolution from the file which can be acquired here.
|
||||
*
|
||||
* Only call after the thread has been opened by Open().
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The true width of the current video file.
|
||||
*/
|
||||
int media_width();
|
||||
|
||||
/**
|
||||
* @brief Retrieve current media height
|
||||
*
|
||||
* See media_width().
|
||||
*
|
||||
* Only call after the thread has been opened by Open().
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The true height of the current video file.
|
||||
*/
|
||||
int media_height();
|
||||
|
||||
/**
|
||||
* @brief Retrieve media time base
|
||||
*
|
||||
* For some timing operations, it's necessary to use the source media's timebase. Similar to media_width() and
|
||||
* media_height(), we need the accurate timebase from the file as a proxy's timebase may or may not be the same
|
||||
* as the source file.
|
||||
*
|
||||
* Only call after the thread has been opened by Open().
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The timebase of the file.
|
||||
*/
|
||||
AVRational media_time_base();
|
||||
|
||||
/**
|
||||
* @brief Wrapper function for queue::lock()
|
||||
*/
|
||||
void QueueLock();
|
||||
|
||||
/**
|
||||
* @brief Wrapper function for queue::unlock()
|
||||
*/
|
||||
void QueueUnlock();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Reference to the parent clip. Set in the constructor and never changed during this object's lifetime.
|
||||
*/
|
||||
ClipPtr clip;
|
||||
|
||||
/**
|
||||
* @brief Frame queue
|
||||
*
|
||||
* Valid fames are cached into this, which also does memory handling when necessary.
|
||||
*/
|
||||
ClipQueue queue;
|
||||
|
||||
/**
|
||||
* @brief Main wait condition
|
||||
*
|
||||
* Used with Clip::cache_lock as the main block while the the Cacher thread isn't running. Wake this condition
|
||||
* to start caching.
|
||||
*/
|
||||
QWaitCondition wait_cond_;
|
||||
|
||||
/**
|
||||
* @brief Main thread wait condition
|
||||
*
|
||||
* Used with main_thread_lock_ to block Cache() while waiting for a response from the cacher thread.
|
||||
*/
|
||||
QWaitCondition main_thread_wait_;
|
||||
|
||||
/**
|
||||
* @brief Main thread mutex
|
||||
*
|
||||
* Used with main_thread_wait_ to block Cache() while waiting for a response from the cacher thread.
|
||||
*/
|
||||
QMutex main_thread_lock_;
|
||||
|
||||
/**
|
||||
* @brief Retrieve() wait condition
|
||||
*
|
||||
* Used with retrieve_lock_ to block Retrieve() if the cacher hasn't retrieved the correct frame yet.
|
||||
*/
|
||||
QWaitCondition retrieve_wait_;
|
||||
|
||||
/**
|
||||
* @brief Retrieve() mutex
|
||||
*
|
||||
* Used with retrieve_wait_ to block Retrieve() if the cacher hasn't retrieved the correct frame yet.
|
||||
*/
|
||||
QMutex retrieve_lock_;
|
||||
|
||||
/**
|
||||
* @brief Set and used by CacheAudioWorker if the decoder receives an EOF.
|
||||
*
|
||||
* Deprecated. CacheAudioWorker() is functional but probably should be rewritten.
|
||||
*/
|
||||
bool reached_end;
|
||||
|
||||
/**
|
||||
* @brief Current Sequence playhead set by Cache()
|
||||
*/
|
||||
long playhead_;
|
||||
|
||||
/**
|
||||
* @brief Current Sequence scrubbing state set by Cache()
|
||||
*/
|
||||
bool scrubbing_;
|
||||
|
||||
/**
|
||||
* @brief Current Sequence playback speed set by Cache()
|
||||
*/
|
||||
int playback_speed_;
|
||||
|
||||
/**
|
||||
* @brief Current nested Sequence hierarchy set by Cache()
|
||||
*/
|
||||
QVector<ClipPtr> nests_;
|
||||
|
||||
/**
|
||||
* @brief Signal cache to continue operation after one cycle rather than wait for another signal
|
||||
*
|
||||
* Each cycle of the cacher thread (see run()) will set this to false in the beginning. Each call of Cache() will set
|
||||
* this to **TRUE**. If this variable is **TRUE**, the cacher won't wait for another signal before starting the next
|
||||
* cache cycle, and will instead just start it.
|
||||
*
|
||||
* Used if Cache() is called and interrupts the cacher while it's already running so that the cacher will restart
|
||||
* itself automatically rather than wait for the next cache signal.
|
||||
*/
|
||||
bool queued_;
|
||||
|
||||
/**
|
||||
* @brief Interrupt the current cache cycle
|
||||
*
|
||||
* A cache cycle will cache several frames at a time. Since decoding can be strenuous and time consuming, the
|
||||
* cycle can be interrupted if it needs to abruptly start caching somewhere else. Best used in tandem with
|
||||
* queued_ to automatically start the next cache cycle.
|
||||
*/
|
||||
bool interrupt_;
|
||||
|
||||
// ffmpeg media handling
|
||||
/**
|
||||
* @brief FFmpeg format/file context - used for media decoding
|
||||
*/
|
||||
AVFormatContext* formatCtx;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg decoder context - used for media decoding
|
||||
*/
|
||||
AVCodecContext* codecCtx;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg stream - used for media decoding
|
||||
*/
|
||||
AVStream* stream;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg packet - used for media decoding
|
||||
*/
|
||||
AVPacket* pkt;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg frame - used for media decoding
|
||||
*
|
||||
* This is usually used as a raw decoded frame before the RGBA conversion/AVFilter stack. Converted/filtered frames go
|
||||
* into Cacher::queue.
|
||||
*/
|
||||
AVFrame* frame_;
|
||||
|
||||
/**
|
||||
* @brief Retrieved frame reference for Retrieve()
|
||||
*
|
||||
* If a frame was found by either Cache() or CacheVideoWorker(), it's set here. If no frame is ready yet, this is set
|
||||
* to `nullptr`.
|
||||
*/
|
||||
AVFrame* retrieved_frame = nullptr;
|
||||
|
||||
// converters/filters
|
||||
/**
|
||||
* @brief FFmpeg filter stack
|
||||
*
|
||||
* Used for conversion from the media's pixel format to RGBA for OpenGL. Also any other FFmpeg filters are implemented
|
||||
* here if necessary (e.g. yadif for deinterlacing). GLSL effects are preferred when available since FFmpeg filters
|
||||
* aren't always fast enough for realtime playback.
|
||||
*/
|
||||
AVFilterGraph* filter_graph;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg buffer source
|
||||
*
|
||||
* Raw decoded frames are added to this for conversion/filtering
|
||||
*/
|
||||
AVFilterContext* buffersrc_ctx;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg buffer sink
|
||||
*
|
||||
* Converted/filtered frames are retrieved from here and sent to Cacher::queue.
|
||||
*/
|
||||
AVFilterContext* buffersink_ctx;
|
||||
|
||||
/**
|
||||
* @brief FFmpeg codec reference
|
||||
*/
|
||||
AVCodec* codec;
|
||||
|
||||
/**
|
||||
* @brief Options set by the cacher for FFmpeg's decoders (settings like multithreading or other optimizations)
|
||||
*/
|
||||
AVDictionary* opts;
|
||||
|
||||
// audio playback variables
|
||||
/**
|
||||
* @brief Internal audio reset variable
|
||||
*
|
||||
* Set by AudioReset() and read by CacheAudioWorker() when the audio state needs to be interrupted and reset.
|
||||
*/
|
||||
bool audio_reset_;
|
||||
|
||||
/**
|
||||
* @brief Internal reverse target variable
|
||||
*
|
||||
* Used by CacheAudioWorker() to stitch audio frames together when reversing. Stores the current frame's timestamp
|
||||
* so it knows how much to decode up to when it backtracks and decodes the next samples.
|
||||
*/
|
||||
int64_t reverse_target_;
|
||||
|
||||
/**
|
||||
* @brief Internal frame sample index variable
|
||||
*
|
||||
* Used by CacheAudioWorker() to mark which part of the audio frame to read from
|
||||
*/
|
||||
int frame_sample_index_;
|
||||
|
||||
/**
|
||||
* @brief Internal audio buffer write variable
|
||||
*
|
||||
* Used by CacheAudioWorker() to mark which part of the audio buffer to write to
|
||||
*/
|
||||
qint64 audio_buffer_write;
|
||||
|
||||
/**
|
||||
* @brief Internal variable that holds the playhead the last time the audio state was reset
|
||||
*/
|
||||
long audio_target_frame;
|
||||
|
||||
/**
|
||||
* @brief Main while loop condition to determine whether thread should continue looping
|
||||
*
|
||||
* Open() sets this to **TRUE**, Close() sets this to **FALSE**. If it's false, the main loop in run() will exit and
|
||||
* the thread will exit cleanly. It's not recommended to set this variable directly, use Open() and Close() instead.
|
||||
*/
|
||||
bool caching_;
|
||||
|
||||
/**
|
||||
* @brief Internal function for opening the file handles and decoder
|
||||
*
|
||||
* After the thread has started, it'll call this function to start all resources necessary for caching. Any
|
||||
* FFmpeg decoding variables and filters are set up here.
|
||||
*
|
||||
* This is
|
||||
* fundamentally different from Open(), this is only meant to be called within the cacher thread and never from
|
||||
* outside and doesn't start the thread like Open() does.
|
||||
*/
|
||||
void OpenWorker();
|
||||
|
||||
/**
|
||||
* @brief Internal function for starting a cache cycle
|
||||
*
|
||||
* This used to have more function, but now just differentiates between CacheVideoWorker() for video clips and
|
||||
* CacheAudioWorker() for audio clips.
|
||||
*/
|
||||
void CacheWorker();
|
||||
|
||||
/**
|
||||
* @brief Internal function for closing cacher
|
||||
*
|
||||
* Called if the main thread loop in run() exits by setting caching_ to **FALSE**. Free's up handles and memory
|
||||
* allocated by OpenWorker().
|
||||
*/
|
||||
void CloseWorker();
|
||||
|
||||
/**
|
||||
* @brief Internal function for resetting audio state
|
||||
*
|
||||
* This used to be a common function, but is now simply a legacy function for CacheAudioWorker(). Resets and
|
||||
* flushes decoders and seeks to the correct timestamp.
|
||||
*/
|
||||
void Reset();
|
||||
|
||||
/**
|
||||
* @brief Internal function for setting retrieved_frame and waking up any threads waiting for it
|
||||
*
|
||||
* @param f
|
||||
*
|
||||
* The frame to set as the retrieved frame.
|
||||
*/
|
||||
void SetRetrievedFrame(AVFrame* f);
|
||||
|
||||
/**
|
||||
* @brief Internal function to wake an external calling thread
|
||||
*
|
||||
* In some situations, Cache() may wait for the cacher to respond before returning. This is to assist in thread
|
||||
* synchronization, making sure the cacher has started working and has locked any resources it needs before any
|
||||
* other threads can access them (e.g. with a function like Retrieve() ). This must be called at the start of
|
||||
* any CacheVideoWorker() or CacheAudioWorker() control paths to ensure the render thread doesn't get stuck.
|
||||
*/
|
||||
void WakeMainThread();
|
||||
|
||||
/**
|
||||
* @brief Retrieve frame from decoder
|
||||
*
|
||||
* Retrieves the next decoded frame from the decoder. Depending on the source media, this frame may or may not be
|
||||
* suitable for usage later in the pipeline as it may or may not be the correct pixel/sample format. For a suitable
|
||||
* frame for the pipeline, use RetrieveFrameAndProcess() instead (which in turn uses this function anyway).
|
||||
*
|
||||
* @param f
|
||||
*
|
||||
* Frame buffer to decode frame into
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* FFmpeg error code (>= 0 on success, a negative error code on failure)
|
||||
*/
|
||||
int RetrieveFrameFromDecoder(AVFrame* f);
|
||||
|
||||
/**
|
||||
* @brief Retrieve frame from decoder and run it through filter stack
|
||||
*
|
||||
* Retrieves the next decoded frame and runs it through the AVFilter stack to create an RGBA frame compatible with
|
||||
* the rest of the pipeline and OpenGL. Use this function if you need a ready-made frame.
|
||||
*
|
||||
* @param f
|
||||
*
|
||||
* The AVFrame to retrieve. This function allocates an AVFrame so you shouldn't do so beforehand. You'll also need to
|
||||
* free it later with av_frame_free() (though ClipQueue will do this automatically if the frame is added to it).
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* FFmpeg error code (>= 0 on success, a negative error code on failure)
|
||||
*/
|
||||
int RetrieveFrameAndProcess(AVFrame *f);
|
||||
|
||||
/**
|
||||
* @brief Internal video caching function
|
||||
*
|
||||
* Performs one video cache cycle. Seeks the media and cleans old frames from the queue if necessary. Decodes frames
|
||||
* and adds them to the queue (after calculating whether they're necessary).
|
||||
*/
|
||||
void CacheVideoWorker();
|
||||
|
||||
/**
|
||||
* @brief Internal audio caching function
|
||||
*
|
||||
* Perform one audio cache cycle. Retrieves audio from decoder, reverses and changes speed if necessary, and sends
|
||||
* audio to the audio buffer which will later be sent to the audio output device.
|
||||
*/
|
||||
void CacheAudioWorker();
|
||||
};
|
||||
|
||||
#endif // CACHER_H
|
||||
@@ -0,0 +1,95 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "clipqueue.h"
|
||||
|
||||
|
||||
ClipQueue::ClipQueue()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
ClipQueue::~ClipQueue()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
void ClipQueue::lock()
|
||||
{
|
||||
queue_lock.lock();
|
||||
}
|
||||
|
||||
bool ClipQueue::tryLock()
|
||||
{
|
||||
return queue_lock.tryLock();
|
||||
}
|
||||
|
||||
void ClipQueue::unlock()
|
||||
{
|
||||
queue_lock.unlock();
|
||||
}
|
||||
|
||||
void ClipQueue::append(AVFrame *frame)
|
||||
{
|
||||
queue.append(frame);
|
||||
}
|
||||
|
||||
AVFrame *ClipQueue::at(int i)
|
||||
{
|
||||
return queue.at(i);
|
||||
}
|
||||
|
||||
AVFrame *ClipQueue::first()
|
||||
{
|
||||
return queue.first();
|
||||
}
|
||||
|
||||
AVFrame *ClipQueue::last()
|
||||
{
|
||||
return queue.last();
|
||||
}
|
||||
|
||||
void ClipQueue::removeFirst()
|
||||
{
|
||||
removeAt(0);
|
||||
}
|
||||
|
||||
void ClipQueue::removeLast()
|
||||
{
|
||||
removeAt(queue.size()-1);
|
||||
}
|
||||
|
||||
void ClipQueue::removeAt(int i)
|
||||
{
|
||||
av_frame_free(&queue[i]);
|
||||
queue.removeAt(i);
|
||||
}
|
||||
|
||||
void ClipQueue::clear()
|
||||
{
|
||||
while (queue.size() > 0) {
|
||||
removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
int ClipQueue::size()
|
||||
{
|
||||
return queue.size();
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef CLIPQUEUE_H
|
||||
#define CLIPQUEUE_H
|
||||
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
}
|
||||
|
||||
#include <QVector>
|
||||
#include <QMutex>
|
||||
|
||||
/**
|
||||
* @brief The ClipQueue class
|
||||
*
|
||||
* A fairly simple wrapper for a QVector and QMutex that cleans up AVFrames automatically when removing them.
|
||||
*/
|
||||
class ClipQueue {
|
||||
public:
|
||||
/**
|
||||
* @brief ClipQueue Constructor
|
||||
*/
|
||||
ClipQueue();
|
||||
|
||||
/**
|
||||
* @brief ClipQueue Destructor
|
||||
*
|
||||
* Automatically clears queue freeing any memory consumed by any AVFrames
|
||||
*/
|
||||
~ClipQueue();
|
||||
|
||||
// Thread safety (QMutex compatible)
|
||||
/**
|
||||
* @brief Lock queue mutex
|
||||
*
|
||||
* Used for multithreading to ensure queue is only accessed by one thread at a time. See QMutex::lock() for more
|
||||
* information.
|
||||
*/
|
||||
void lock();
|
||||
|
||||
/**
|
||||
* @brief Try to lock queue mutex
|
||||
*
|
||||
* Used for multithreading to ensure queue is only accessed by one thread at a time. Tries to lock, but doesn't block
|
||||
* the calling thread and wait if it can't lock it. See QMutex::tryLock() for more information.
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* **TRUE** if the lock succeeded, **FALSE** if not.
|
||||
*/
|
||||
bool tryLock();
|
||||
|
||||
/**
|
||||
* @brief Unlock queue mutex
|
||||
*
|
||||
* Used for multithreading to ensure queue is only accessed by one thread at a time. See QMutex::unlock() for more
|
||||
* information.
|
||||
*/
|
||||
void unlock();
|
||||
|
||||
// Array handling (QVector compatible)
|
||||
/**
|
||||
* @brief Add a frame to the end of the queue
|
||||
*
|
||||
* @param frame
|
||||
*
|
||||
* The frame to add
|
||||
*/
|
||||
void append(AVFrame* frame);
|
||||
|
||||
/**
|
||||
* @brief Retrieve a frame at a certain index
|
||||
*
|
||||
* @param i
|
||||
*
|
||||
* Index to retrieve frame from
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* AVFrame at this index
|
||||
*/
|
||||
AVFrame* at(int i);
|
||||
|
||||
/**
|
||||
* @brief Retrieve first frame in the queue
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The first AVFrame in the queue
|
||||
*/
|
||||
AVFrame* first();
|
||||
|
||||
/**
|
||||
* @brief Retrieve last frame in the queue
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The last AVFrame in the queue
|
||||
*/
|
||||
AVFrame* last();
|
||||
|
||||
/**
|
||||
* @brief Remove first frame in the queue
|
||||
*
|
||||
* Frees all memory occupied by this frame and removes it from the queue
|
||||
*/
|
||||
void removeFirst();
|
||||
|
||||
/**
|
||||
* @brief Remove last frame in the queue
|
||||
*
|
||||
* Frees all memory occupied by this frame and removes it from the queue
|
||||
*/
|
||||
void removeLast();
|
||||
|
||||
/**
|
||||
* @brief Remove frame in the queue at a certain index
|
||||
*
|
||||
* Frees all memory occupied by this frame and removes it from the queue
|
||||
*
|
||||
* @param i
|
||||
*
|
||||
* Index to remove a frame at
|
||||
*/
|
||||
void removeAt(int i);
|
||||
|
||||
/**
|
||||
* @brief Clear entire queue
|
||||
*
|
||||
* Frees all memory occupied by all frames and clears the entire queue
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Retrieve current size of the queue
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Current the current size of the queue. All indexes in the queue are guaranteed to be valid references to an
|
||||
* AVFrame.
|
||||
*/
|
||||
int size();
|
||||
|
||||
private:
|
||||
QVector<AVFrame*> queue;
|
||||
QMutex queue_lock;
|
||||
};
|
||||
|
||||
#endif // CLIPQUEUE_H
|
||||
@@ -0,0 +1,736 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderfunctions.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
}
|
||||
|
||||
#include <QOpenGLFramebufferObject>
|
||||
#include <QApplication>
|
||||
#include <QDesktopWidget>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef OLIVE_OCIO
|
||||
#include <OpenColorIO/OpenColorIO.h>
|
||||
namespace OCIO = OCIO_NAMESPACE;
|
||||
#endif
|
||||
|
||||
#include "project/clip.h"
|
||||
#include "project/sequence.h"
|
||||
#include "project/media.h"
|
||||
#include "project/effect.h"
|
||||
#include "project/footage.h"
|
||||
#include "project/transition.h"
|
||||
|
||||
#include "ui/collapsiblewidget.h"
|
||||
|
||||
#include "rendering/audio.h"
|
||||
|
||||
#include "io/math.h"
|
||||
#include "io/config.h"
|
||||
|
||||
#include "panels/timeline.h"
|
||||
#include "panels/viewer.h"
|
||||
|
||||
const int kMaximumRetryCount = 10;
|
||||
|
||||
void full_blit() {
|
||||
glPushMatrix();
|
||||
glLoadIdentity();
|
||||
glOrtho(0, 1, 0, 1, -1, 1);
|
||||
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0, 0); // top left
|
||||
glVertex2f(0, 0); // top left
|
||||
glTexCoord2f(1, 0); // top right
|
||||
glVertex2f(1, 0); // top right
|
||||
glTexCoord2f(1, 1); // bottom right
|
||||
glVertex2f(1, 1); // bottom right
|
||||
glTexCoord2f(0, 1); // bottom left
|
||||
glVertex2f(0, 1); // bottom left
|
||||
glEnd();
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
void draw_clip(QOpenGLContext* ctx, GLuint fbo, GLuint texture, bool clear) {
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
|
||||
if (clear) {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
full_blit();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
GLuint draw_clip(QOpenGLFramebufferObject* fbo, GLuint texture, bool clear) {
|
||||
fbo->bind();
|
||||
|
||||
if (clear) {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
full_blit();
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
fbo->release();
|
||||
|
||||
return fbo->texture();
|
||||
}
|
||||
|
||||
void process_effect(ClipPtr c,
|
||||
EffectPtr e,
|
||||
double timecode,
|
||||
GLTextureCoords& coords,
|
||||
GLuint& composite_texture,
|
||||
bool& fbo_switcher,
|
||||
bool& texture_failed,
|
||||
int data) {
|
||||
if (e->is_enabled()) {
|
||||
if (e->enable_coords) {
|
||||
e->process_coords(timecode, coords, data);
|
||||
}
|
||||
bool can_process_shaders = (e->enable_shader && olive::CurrentRuntimeConfig.shaders_are_enabled);
|
||||
if (can_process_shaders || e->enable_superimpose) {
|
||||
e->startEffect();
|
||||
if (can_process_shaders && e->is_glsl_linked()) {
|
||||
for (int i=0;i<e->getIterations();i++) {
|
||||
e->process_shader(timecode, coords, i);
|
||||
composite_texture = draw_clip(c->fbo[fbo_switcher], composite_texture, true);
|
||||
fbo_switcher = !fbo_switcher;
|
||||
}
|
||||
}
|
||||
if (e->enable_superimpose) {
|
||||
GLuint superimpose_texture = e->process_superimpose(timecode);
|
||||
|
||||
if (superimpose_texture == 0) {
|
||||
qWarning() << "Superimpose texture was nullptr, retrying...";
|
||||
texture_failed = true;
|
||||
} else if (composite_texture == 0) {
|
||||
// if there is no previous texture, just return the superimposes texture
|
||||
// UNLESS this is a shader-extended superimpose effect in which case,
|
||||
// we'll need to draw it below
|
||||
composite_texture = superimpose_texture;
|
||||
} else {
|
||||
// if the source texture is not already a framebuffer texture,
|
||||
// we'll need to make it one before drawing a superimpose effect on it
|
||||
if (composite_texture != c->fbo[0]->texture() && composite_texture != c->fbo[1]->texture()) {
|
||||
draw_clip(c->fbo[!fbo_switcher], composite_texture, true);
|
||||
}
|
||||
|
||||
composite_texture = draw_clip(c->fbo[!fbo_switcher], superimpose_texture, false);
|
||||
}
|
||||
}
|
||||
e->endEffect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint compose_sequence(ComposeSequenceParams ¶ms) {
|
||||
// qint64 time = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
GLuint final_fbo = params.main_buffer;
|
||||
|
||||
SequencePtr s = params.seq;
|
||||
long playhead = s->playhead;
|
||||
|
||||
if (!params.nests.isEmpty()) {
|
||||
for (int i=0;i<params.nests.size();i++) {
|
||||
s = params.nests.at(i)->media()->to_sequence();
|
||||
playhead += params.nests.at(i)->clip_in(true) - params.nests.at(i)->timeline_in(true);
|
||||
playhead = rescale_frame_number(playhead, params.nests.at(i)->sequence->frame_rate, s->frame_rate);
|
||||
}
|
||||
|
||||
if (params.video && params.nests.last()->fbo != nullptr) {
|
||||
params.nests.last()->fbo[0]->bind();
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
final_fbo = params.nests.last()->fbo[0]->handle();
|
||||
}
|
||||
}
|
||||
|
||||
int audio_track_count = 0;
|
||||
|
||||
QVector<ClipPtr> current_clips;
|
||||
|
||||
// loop through clips, find currently active, and sort by track
|
||||
for (int i=0;i<s->clips.size();i++) {
|
||||
|
||||
ClipPtr c = s->clips.at(i);
|
||||
|
||||
if (c != nullptr) {
|
||||
|
||||
// if clip is video and we're processing video
|
||||
if ((c->track() < 0) == params.video) {
|
||||
|
||||
bool clip_is_active = false;
|
||||
|
||||
// is the clip a "footage" clip?
|
||||
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
|
||||
FootagePtr m = c->media()->to_footage();
|
||||
|
||||
// does the clip have a valid media source?
|
||||
if (!m->invalid && !(c->track() >= 0 && !is_audio_device_set())) {
|
||||
|
||||
// is the media process and ready?
|
||||
if (m->ready) {
|
||||
const FootageStream* ms = c->media_stream();
|
||||
|
||||
// does the media have a valid media stream source and is it active?
|
||||
if (ms != nullptr && c->IsActiveAt(playhead)) {
|
||||
|
||||
// open if not open
|
||||
if (!c->IsOpen()) {
|
||||
c->Open();
|
||||
}
|
||||
|
||||
clip_is_active = true;
|
||||
|
||||
// increment audio track count
|
||||
if (c->track() >= 0) audio_track_count++;
|
||||
|
||||
} else if (c->IsOpen()) {
|
||||
|
||||
// close the clip if it isn't active anymore
|
||||
c->Close(false);
|
||||
|
||||
}
|
||||
} else {
|
||||
|
||||
// media wasn't ready, schedule a redraw
|
||||
params.texture_failed = true;
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if the clip is a nested sequence or null clip, just open it
|
||||
|
||||
if (c->IsActiveAt(playhead)) {
|
||||
if (!c->IsOpen()) {
|
||||
c->Open();
|
||||
}
|
||||
clip_is_active = true;
|
||||
} else if (c->IsOpen()) {
|
||||
c->Close(false);
|
||||
}
|
||||
}
|
||||
|
||||
// if the clip is active, added it to "current_clips", sorted by track
|
||||
if (clip_is_active) {
|
||||
bool added = false;
|
||||
|
||||
// track sorting is only necessary for video clips
|
||||
// audio clips are mixed equally, so we skip sorting for those
|
||||
if (params.video) {
|
||||
|
||||
// insertion sort by track
|
||||
for (int j=0;j<current_clips.size();j++) {
|
||||
if (current_clips.at(j)->track() < c->track()) {
|
||||
current_clips.insert(j, c);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
current_clips.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params.video) {
|
||||
// set default coordinates based on the sequence, with 0 in the direct center
|
||||
glPushMatrix();
|
||||
glLoadIdentity();
|
||||
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
int half_width = s->width/2;
|
||||
int half_height = s->height/2;
|
||||
glOrtho(-half_width, half_width, -half_height, half_height, -1, 10);
|
||||
}
|
||||
|
||||
// loop through current clips
|
||||
|
||||
for (int i=0;i<current_clips.size();i++) {
|
||||
ClipPtr c = current_clips.at(i);
|
||||
|
||||
bool got_mutex = true;
|
||||
|
||||
if (params.wait_for_mutexes) {
|
||||
// wait for clip to finish opening
|
||||
c->state_change_lock.lock();
|
||||
} else {
|
||||
got_mutex = c->state_change_lock.tryLock();
|
||||
}
|
||||
|
||||
if (got_mutex && c->IsOpen()) {
|
||||
// if clip is a video clip
|
||||
if (c->track() < 0) {
|
||||
|
||||
// reset OpenGL to full color
|
||||
glColor4f(1.0, 1.0, 1.0, 1.0);
|
||||
|
||||
// textureID variable contains texture to be drawn on screen at the end
|
||||
GLuint textureID = 0;
|
||||
|
||||
// store video source dimensions
|
||||
int video_width = c->media_width();
|
||||
int video_height = c->media_height();
|
||||
|
||||
// if media is footage
|
||||
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
|
||||
|
||||
// retrieve video frame from cache and store it in c->texture
|
||||
c->Cache(qMax(playhead, c->timeline_in()), false, false, params.nests, params.playback_speed);
|
||||
if (!c->Retrieve()) {
|
||||
params.texture_failed = true;
|
||||
} else {
|
||||
// retrieve ID from c->texture
|
||||
textureID = c->texture->textureId();
|
||||
}
|
||||
|
||||
if (textureID == 0) {
|
||||
qWarning() << "Failed to create texture";
|
||||
}
|
||||
}
|
||||
|
||||
// prepare framebuffers for backend drawing operations
|
||||
if (c->fbo == nullptr) {
|
||||
// create 3 fbos for nested sequences, 2 for most clips
|
||||
int fbo_count = (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE) ? 3 : 2;
|
||||
|
||||
c->fbo = new QOpenGLFramebufferObject* [size_t(fbo_count)];
|
||||
|
||||
for (int j=0;j<fbo_count;j++) {
|
||||
c->fbo[j] = new QOpenGLFramebufferObject(video_width, video_height);
|
||||
}
|
||||
}
|
||||
|
||||
// if clip should actually be shown on screen in this frame
|
||||
if (playhead >= c->timeline_in(true)
|
||||
&& playhead < c->timeline_out(true)) {
|
||||
glPushMatrix();
|
||||
|
||||
// simple bool for switching between the two framebuffers
|
||||
bool fbo_switcher = false;
|
||||
|
||||
glViewport(0, 0, video_width, video_height);
|
||||
|
||||
if (c->media() != nullptr) {
|
||||
if (c->media()->get_type() == MEDIA_TYPE_SEQUENCE) {
|
||||
// for a nested sequence, run this function again on that sequence and retrieve the texture
|
||||
|
||||
// add nested sequence to nest list
|
||||
params.nests.append(c);
|
||||
|
||||
// compose sequence
|
||||
textureID = compose_sequence(params);
|
||||
|
||||
// remove sequence from nest list
|
||||
params.nests.removeLast();
|
||||
|
||||
// compose_sequence() would have written to this clip's fbo[0], so we switch to fbo[1]
|
||||
fbo_switcher = true;
|
||||
} else if (c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
|
||||
|
||||
if (!c->media()->to_footage()->alpha_is_premultiplied) {
|
||||
// alpha is not premultiplied, we'll need to multiply it for the rest of the pipeline
|
||||
params.premultiply_program->bind();
|
||||
|
||||
textureID = draw_clip(c->fbo[0], textureID, true);
|
||||
|
||||
params.premultiply_program->release();
|
||||
|
||||
fbo_switcher = true;
|
||||
}
|
||||
|
||||
#ifdef OLIVE_OCIO
|
||||
// convert to linear colorspace
|
||||
bool linear_convert = true;
|
||||
if (linear_convert)
|
||||
{
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// set up default coordinates for drawing the clip
|
||||
GLTextureCoords coords;
|
||||
coords.grid_size = 1;
|
||||
coords.vertexTopLeftX = coords.vertexBottomLeftX = -video_width/2;
|
||||
coords.vertexTopLeftY = coords.vertexTopRightY = -video_height/2;
|
||||
coords.vertexTopRightX = coords.vertexBottomRightX = video_width/2;
|
||||
coords.vertexBottomLeftY = coords.vertexBottomRightY = video_height/2;
|
||||
coords.vertexBottomLeftZ = coords.vertexBottomRightZ = coords.vertexTopLeftZ = coords.vertexTopRightZ = 1;
|
||||
coords.textureTopLeftY = coords.textureTopRightY = coords.textureTopLeftX = coords.textureBottomLeftX = 0.0;
|
||||
coords.textureBottomLeftY = coords.textureBottomRightY = coords.textureTopRightX = coords.textureBottomRightX = 1.0;
|
||||
coords.textureTopLeftQ = coords.textureTopRightQ = coords.textureTopLeftQ = coords.textureBottomLeftQ = 1;
|
||||
coords.blendmode = BLEND_MODE_NORMAL;
|
||||
coords.opacity = 1.0;
|
||||
|
||||
// if auto-scale is enabled, auto-scale the clip
|
||||
if (c->autoscaled() && (video_width != s->width && video_height != s->height)) {
|
||||
float width_multiplier = float(s->width) / float(video_width);
|
||||
float height_multiplier = float(s->height) / float(video_height);
|
||||
float scale_multiplier = qMin(width_multiplier, height_multiplier);
|
||||
glScalef(scale_multiplier, scale_multiplier, 1);
|
||||
}
|
||||
|
||||
// == EFFECT CODE START ==
|
||||
|
||||
// get current sequence time in seconds (used for effects)
|
||||
double timecode = get_timecode(c, playhead);
|
||||
|
||||
// set up variables for gizmos later
|
||||
EffectPtr first_gizmo_effect = nullptr;
|
||||
EffectPtr selected_effect = nullptr;
|
||||
|
||||
// run through all of the clip's effects
|
||||
for (int j=0;j<c->effects.size();j++) {
|
||||
EffectPtr e = c->effects.at(j);
|
||||
process_effect(c, e, timecode, coords, textureID, fbo_switcher, params.texture_failed, kTransitionNone);
|
||||
|
||||
// retrieve gizmo data from effect
|
||||
if (e->are_gizmos_enabled()) {
|
||||
if (first_gizmo_effect == nullptr) first_gizmo_effect = e;
|
||||
if (e->container->selected) selected_effect = e;
|
||||
}
|
||||
}
|
||||
|
||||
// using gizmo data, set definitive gizmo
|
||||
if (selected_effect != nullptr) {
|
||||
(*params.gizmos) = selected_effect;
|
||||
} else if (is_clip_selected(c, true)) {
|
||||
(*params.gizmos) = first_gizmo_effect;
|
||||
}
|
||||
|
||||
// if the clip has an opening transition, process that now
|
||||
if (c->opening_transition != nullptr) {
|
||||
int transition_progress = playhead - c->timeline_in(true);
|
||||
if (transition_progress < c->opening_transition->get_length()) {
|
||||
process_effect(c, c->opening_transition, double(transition_progress)/double(c->opening_transition->get_length()), coords, textureID, fbo_switcher, params.texture_failed, kTransitionOpening);
|
||||
}
|
||||
}
|
||||
|
||||
// if the clip has a closing transition, process that now
|
||||
if (c->closing_transition != nullptr) {
|
||||
int transition_progress = playhead - (c->timeline_out(true) - c->closing_transition->get_length());
|
||||
if (transition_progress >= 0 && transition_progress < c->closing_transition->get_length()) {
|
||||
process_effect(c, c->closing_transition, double(transition_progress)/double(c->closing_transition->get_length()), coords, textureID, fbo_switcher, params.texture_failed, kTransitionClosing);
|
||||
}
|
||||
}
|
||||
|
||||
// == EFFECT CODE END ==
|
||||
|
||||
if (textureID > 0) {
|
||||
// set viewport to sequence size
|
||||
params.ctx->functions()->glViewport(0, 0, s->width, s->height);
|
||||
|
||||
|
||||
|
||||
// == START RENDER CLIP IN CONTEXT OF SEQUENCE ==
|
||||
|
||||
|
||||
|
||||
// use clip textures for nested sequences, otherwise use main frame buffers
|
||||
GLuint back_buffer_1;
|
||||
GLuint backend_tex_1;
|
||||
GLuint backend_tex_2;
|
||||
if (params.nests.size() > 0) {
|
||||
back_buffer_1 = params.nests.last()->fbo[1]->handle();
|
||||
backend_tex_1 = params.nests.last()->fbo[1]->texture();
|
||||
backend_tex_2 = params.nests.last()->fbo[2]->texture();
|
||||
} else {
|
||||
back_buffer_1 = params.backend_buffer1;
|
||||
backend_tex_1 = params.backend_attachment1;
|
||||
backend_tex_2 = params.backend_attachment2;
|
||||
}
|
||||
|
||||
// render a backbuffer
|
||||
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, back_buffer_1);
|
||||
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// bind final clip texture
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
|
||||
// set texture filter to bilinear
|
||||
params.ctx->functions()->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
params.ctx->functions()->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
// draw clip on screen according to gl coordinates
|
||||
glBegin(GL_QUADS);
|
||||
|
||||
glTexCoord2f(coords.textureTopLeftX, coords.textureTopLeftY); // top left
|
||||
glVertex2f(coords.vertexTopLeftX, coords.vertexTopLeftY); // top left
|
||||
glTexCoord2f(coords.textureTopRightX, coords.textureTopRightY); // top right
|
||||
glVertex2f(coords.vertexTopRightX, coords.vertexTopRightY); // top right
|
||||
glTexCoord2f(coords.textureBottomRightX, coords.textureBottomRightY); // bottom right
|
||||
glVertex2f(coords.vertexBottomRightX, coords.vertexBottomRightY); // bottom right
|
||||
glTexCoord2f(coords.textureBottomLeftX, coords.textureBottomLeftY); // bottom left
|
||||
glVertex2f(coords.vertexBottomLeftX, coords.vertexBottomLeftY); // bottom left
|
||||
|
||||
glEnd();
|
||||
|
||||
// release final clip texture
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
|
||||
|
||||
|
||||
// == END RENDER CLIP IN CONTEXT OF SEQUENCE ==
|
||||
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// PROCESS POST-SHADERS
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
|
||||
// copy front buffer to back buffer (only if we're using blending modes - which we usually will be)
|
||||
if (!olive::CurrentRuntimeConfig.disable_blending) {
|
||||
if (params.nests.size() > 0) {
|
||||
draw_clip(params.ctx, params.nests.last()->fbo[2]->handle(), params.nests.last()->fbo[0]->texture(), true);
|
||||
} else {
|
||||
draw_clip(params.ctx, params.backend_buffer2, params.main_attachment, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// == START FINAL DRAW ON SEQUENCE BUFFER ==
|
||||
|
||||
|
||||
|
||||
// thread safety (see docs for ComposeSequenceParams::main_buffer_mutex)
|
||||
if (final_fbo == params.main_buffer) {
|
||||
params.main_buffer_mutex->lock();
|
||||
}
|
||||
|
||||
// bind front buffer as draw buffer
|
||||
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, final_fbo);
|
||||
|
||||
if (olive::CurrentRuntimeConfig.disable_blending) {
|
||||
// some GPUs don't like the blending shader, so we provide a pure GL fallback here
|
||||
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_1);
|
||||
|
||||
glColor4f(coords.opacity, coords.opacity, coords.opacity, coords.opacity);
|
||||
|
||||
full_blit();
|
||||
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
|
||||
} else {
|
||||
// load background texture into texture unit 0
|
||||
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 0); // Texture unit 0
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_2);
|
||||
|
||||
// load foreground texture into texture unit 1
|
||||
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 1); // Texture unit 1
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_1);
|
||||
|
||||
// bind and configure blending mode shader
|
||||
params.blend_mode_program->bind();
|
||||
params.blend_mode_program->setUniformValue("blendmode", coords.blendmode);
|
||||
params.blend_mode_program->setUniformValue("opacity", coords.opacity);
|
||||
params.blend_mode_program->setUniformValue("background", 0);
|
||||
params.blend_mode_program->setUniformValue("foreground", 1);
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
full_blit();
|
||||
|
||||
// release blend mode shader
|
||||
params.blend_mode_program->release();
|
||||
|
||||
// unbind texture from texture unit 1
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// unbind texture from texture unit 0
|
||||
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 0); // Texture unit 0
|
||||
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// unbind framebuffer
|
||||
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
|
||||
// thread safety (see docs for ComposeSequenceParams::main_buffer_mutex)
|
||||
if (final_fbo == params.main_buffer) {
|
||||
params.main_buffer_mutex->unlock();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// == END FINAL DRAW ON SEQUENCE BUFFER ==
|
||||
}
|
||||
|
||||
// prepare gizmos
|
||||
if ((*params.gizmos) != nullptr
|
||||
&& params.nests.isEmpty()
|
||||
&& ((*params.gizmos) == first_gizmo_effect
|
||||
|| (*params.gizmos) == selected_effect)) {
|
||||
(*params.gizmos)->gizmo_draw(timecode, coords); // set correct gizmo coords
|
||||
(*params.gizmos)->gizmo_world_to_screen(); // convert gizmo coords to screen coords
|
||||
}
|
||||
|
||||
glPopMatrix();
|
||||
}
|
||||
} else {
|
||||
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE) {
|
||||
params.nests.append(c);
|
||||
compose_sequence(params);
|
||||
params.nests.removeLast();
|
||||
} else {
|
||||
// Check whether cacher is currently active, if not activate it now
|
||||
if (c->cache_lock.tryLock()) {
|
||||
|
||||
c->cache_lock.unlock();
|
||||
|
||||
c->Cache(playhead,
|
||||
false,
|
||||
(params.viewer != nullptr && !params.viewer->playing),
|
||||
params.nests,
|
||||
params.playback_speed);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// visually update all the keyframe values
|
||||
if (c->sequence == params.seq) { // only if you can currently see them
|
||||
double ts = (playhead - c->timeline_in(true) + c->clip_in(true))/s->frame_rate;
|
||||
for (int i=0;i<c->effects.size();i++) {
|
||||
EffectPtr e = c->effects.at(i);
|
||||
for (int j=0;j<e->row_count();j++) {
|
||||
EffectRow* r = e->row(j);
|
||||
for (int k=0;k<r->fieldCount();k++) {
|
||||
r->field(k)->validate_keyframe_data(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
params.texture_failed = true;
|
||||
}
|
||||
|
||||
if (got_mutex) {
|
||||
c->state_change_lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_track_count == 0 && params.viewer != nullptr) {
|
||||
params.viewer->play_wake();
|
||||
}
|
||||
|
||||
if (params.video) {
|
||||
glPopMatrix();
|
||||
}
|
||||
|
||||
// qDebug() << "compose sequence took" << QDateTime::currentMSecsSinceEpoch() - time;
|
||||
|
||||
if (!params.nests.isEmpty() && params.nests.last()->fbo != nullptr) {
|
||||
// returns nested clip's texture
|
||||
return params.nests.last()->fbo[0]->texture();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void compose_audio(Viewer* viewer, SequencePtr seq, int playback_speed, bool wait_for_mutexes) {
|
||||
ComposeSequenceParams params;
|
||||
params.viewer = viewer;
|
||||
params.ctx = nullptr;
|
||||
params.seq = seq;
|
||||
params.video = false;
|
||||
params.gizmos = nullptr;
|
||||
params.wait_for_mutexes = wait_for_mutexes;
|
||||
params.playback_speed = playback_speed;
|
||||
params.blend_mode_program = nullptr;
|
||||
compose_sequence(params);
|
||||
}
|
||||
|
||||
long rescale_frame_number(long framenumber, double source_frame_rate, double target_frame_rate) {
|
||||
return qRound((double(framenumber)/source_frame_rate)*target_frame_rate);
|
||||
}
|
||||
|
||||
double get_timecode(ClipPtr c, long playhead) {
|
||||
return double(playhead_to_clip_frame(c, playhead))/c->sequence->frame_rate;
|
||||
}
|
||||
|
||||
long playhead_to_clip_frame(ClipPtr c, long playhead) {
|
||||
return (qMax(0L, playhead - c->timeline_in(true)) + c->clip_in(true));
|
||||
}
|
||||
|
||||
double playhead_to_clip_seconds(ClipPtr c, long playhead) {
|
||||
// returns time in seconds
|
||||
long clip_frame = playhead_to_clip_frame(c, playhead);
|
||||
|
||||
if (c->reversed()) {
|
||||
clip_frame = c->media_length() - clip_frame - 1;
|
||||
}
|
||||
|
||||
double secs = (double(clip_frame)/c->sequence->frame_rate)*c->speed().value;
|
||||
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
|
||||
secs *= c->media()->to_footage()->speed;
|
||||
}
|
||||
|
||||
return secs;
|
||||
}
|
||||
|
||||
int64_t seconds_to_timestamp(ClipPtr c, double seconds) {
|
||||
return qRound64(seconds * av_q2d(av_inv_q(c->time_base())));
|
||||
}
|
||||
|
||||
int64_t playhead_to_timestamp(ClipPtr c, long playhead) {
|
||||
return seconds_to_timestamp(c, playhead_to_clip_seconds(c, playhead));
|
||||
}
|
||||
|
||||
void close_active_clips(SequencePtr s) {
|
||||
if (s != nullptr) {
|
||||
for (int i=0;i<s->clips.size();i++) {
|
||||
ClipPtr c = s->clips.at(i);
|
||||
if (c != nullptr) {
|
||||
c->Close(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERFUNCTIONS_H
|
||||
#define RENDERFUNCTIONS_H
|
||||
|
||||
#include <QOpenGLContext>
|
||||
#include <QVector>
|
||||
#include <QOpenGLShaderProgram>
|
||||
|
||||
#include "project/sequence.h"
|
||||
#include "project/effect.h"
|
||||
|
||||
#include "panels/viewer.h"
|
||||
|
||||
/**
|
||||
* @brief The ComposeSequenceParams struct
|
||||
*
|
||||
* Struct sent to the compose_sequence() function.
|
||||
*/
|
||||
struct ComposeSequenceParams {
|
||||
|
||||
/**
|
||||
* @brief Reference to the Viewer class that's calling compose_sequence()
|
||||
*
|
||||
* Primarily used for calling Viewer::play_wake() when appropriate.
|
||||
*/
|
||||
Viewer* viewer;
|
||||
|
||||
/**
|
||||
* @brief The OpenGL context to use while rendering.
|
||||
*
|
||||
* For video rendering, this must be a valid OpenGL context. For audio, this variable is never accessed.
|
||||
*
|
||||
* \see ComposeSequenceParams::video
|
||||
*/
|
||||
QOpenGLContext* ctx;
|
||||
|
||||
/**
|
||||
* @brief The sequence to compose
|
||||
*
|
||||
* In addition to clips, sequences also contain the playhead position so compose_sequence() knows which frame
|
||||
* to render.
|
||||
*/
|
||||
SequencePtr seq;
|
||||
|
||||
/**
|
||||
* @brief Array to store the nested sequence hierarchy
|
||||
*
|
||||
* Should be left empty. This array gets passed around compose_sequence() as it calls itself recursively to
|
||||
* handle nested sequences.
|
||||
*/
|
||||
QVector<ClipPtr> nests;
|
||||
|
||||
/**
|
||||
* @brief Set compose mode to video or audio
|
||||
*
|
||||
* **TRUE** if this function should render video, **FALSE** if this function should render audio.
|
||||
*/
|
||||
bool video;
|
||||
|
||||
/**
|
||||
* @brief Set to the Effect whose gizmos were chosen to be drawn on screen
|
||||
*
|
||||
* A pointer to a pointer that will be set to the Effect whose gizmos are being rendered and should therefore
|
||||
* be interacted with if the user uses them.
|
||||
*/
|
||||
EffectPtr* gizmos;
|
||||
|
||||
/**
|
||||
* @brief A variable that compose_sequence() will set to **TRUE** if any of the clips couldn't be shown.
|
||||
*
|
||||
* A footage item or shader may not be ready at the time this frame is drawn. If compose_sequence() couldn't draw
|
||||
* any of the clips in the scene, this variable is set to **TRUE** indicating that the image rendered is a
|
||||
* "best effort", but not the actual image.
|
||||
*
|
||||
* This variable should be checked after compose_sequence() and a repaint should be triggered if it's **TRUE**.
|
||||
*
|
||||
* \note This variable is probably bad design and is a relic of an earlier rendering backend. There may be a better
|
||||
* way to communicate this information.
|
||||
*
|
||||
* Additionally, since
|
||||
* compose_sequence() for video will now always run in a separate thread anyway, there's no real issue with
|
||||
* stalling it to wait for footage to complete opening or whatever may be lagging behind. A possible side effect
|
||||
* of this though is that the preview may become less responsive if it's stuck trying to render one frame. With
|
||||
* the current system, the preview may show incomplete frames occasionally but at least it will show something.
|
||||
* This may be preferable. See ComposeSequenceParams::single_threaded for a similar function that could be
|
||||
* removed.
|
||||
*/
|
||||
bool texture_failed;
|
||||
|
||||
/**
|
||||
* @brief Run all cachers in the same thread that compose_sequence() is in
|
||||
*
|
||||
* Standard behavior is that all clips cache frames in their own thread and signals are sent between
|
||||
* compose_sequence() and the clip's cacher thread regarding which frames to display and cache without stalling
|
||||
* the compose_sequence() thread. Setting this to **TRUE** will run all cachers in the same thread creating a
|
||||
* technically more "perfect" connection between them that will also stall the compose_sequence() thread. Used
|
||||
* when rendering as timing isn't as important as creating output frames as quickly as possible.
|
||||
*
|
||||
* \note Exporting should probably be rewritten without this. While running all the cachers in one thread makes
|
||||
* it easier to synchronize everything, export performance could probably benefit from keeping them in separate
|
||||
* threads and syncing up with them. See ComposeSequenceParams::texture_failed for a similar function that could
|
||||
* be removed.
|
||||
*/
|
||||
bool wait_for_mutexes;
|
||||
|
||||
/**
|
||||
* @brief Set the current playback speed (adjusted with Shuttle Left/Right)
|
||||
*
|
||||
* Only used for audio rendering to determine how many samples to skip in order to play audio at the correct speed.
|
||||
*
|
||||
* \see ComposeSequenceParams::video
|
||||
*/
|
||||
int playback_speed;
|
||||
|
||||
/**
|
||||
* @brief Blending mode shader
|
||||
*
|
||||
* Used only for video rendering. Never accessed with audio rendering.
|
||||
*
|
||||
* A program containing the current active
|
||||
* blending mode shader that can be bound during rendering. Must be compiled and linked beforehand. See
|
||||
* RenderThread::blend_mode_program for how this is properly set up.
|
||||
*
|
||||
* \see ComposeSequenceParams::video
|
||||
*/
|
||||
QOpenGLShaderProgram* blend_mode_program;
|
||||
|
||||
/**
|
||||
* @brief Premultiply alpha shader
|
||||
*
|
||||
* Used only for video rendering. Never accessed with audio rendering.
|
||||
*
|
||||
* compose_sequence()'s internal composition
|
||||
* expects premultipled alpha, but it will pre-emptively multiply any footage that is not set as already
|
||||
* premultiplied (see Footage::alpha_is_premultiplied) using this shader. Must be compiled and linked beforehand.
|
||||
* See RenderThread::premultiply_program for how this is properly set up.
|
||||
*/
|
||||
QOpenGLShaderProgram* premultiply_program;
|
||||
|
||||
/**
|
||||
* @brief The OpenGL framebuffer object that the final texture to be shown is rendered to.
|
||||
*
|
||||
* Used only for video rendering. Never accessed with audio rendering.
|
||||
*
|
||||
* When compose_sequence() is rendering the final image, this framebuffer will be bound.
|
||||
*/
|
||||
GLuint main_buffer;
|
||||
|
||||
/**
|
||||
* @brief The attachment to the framebuffer in main_buffer
|
||||
*
|
||||
* Used only for video rendering. Never accessed with audio rendering.
|
||||
*
|
||||
* The OpenGL texture attached to the framebuffer referenced by main_buffer.
|
||||
*/
|
||||
GLuint main_attachment;
|
||||
|
||||
/**
|
||||
* @brief Mutex for the main framebuffer
|
||||
*
|
||||
* Used only for video rendering. Never accessed with audio rendering.
|
||||
*
|
||||
* If this is not nullptr, compose_sequence() will lock this mutex when rendering to main_buffer/main_attachment.
|
||||
* Used to synchronize ViewerWidget and ViewerWindow with RenderThread.
|
||||
*/
|
||||
QMutex* main_buffer_mutex;
|
||||
|
||||
/**
|
||||
* @brief Backend OpenGL framebuffer 1 used for further processing before rendering to main_buffer
|
||||
*
|
||||
* In some situations, compose_sequence() will do some processing through shaders that requires "ping-ponging"
|
||||
* between framebuffers. backend_buffer1 and backend_buffer2 are used for this purpose.
|
||||
*/
|
||||
GLuint backend_buffer1;
|
||||
|
||||
/**
|
||||
* @brief Backend OpenGL framebuffer 1's texture attachment
|
||||
*
|
||||
* The texture that ComposeSequenceParams::backend_buffer1 renders to. Bound and drawn to
|
||||
* ComposeSequenceParams::backend_buffer2 to "ping-pong" between them and various shaders.
|
||||
*/
|
||||
GLuint backend_attachment1;
|
||||
|
||||
/**
|
||||
* @brief Backend OpenGL framebuffer 2 used for further processing before rendering to main_buffer
|
||||
*
|
||||
* In some situations, compose_sequence() will do some processing through shaders that requires "ping-ponging"
|
||||
* between framebuffers. backend_buffer1 and backend_buffer2 are used for this purpose.
|
||||
*/
|
||||
GLuint backend_buffer2;
|
||||
|
||||
/**
|
||||
* @brief Backend OpenGL framebuffer 2's texture attachment
|
||||
*
|
||||
* The texture that ComposeSequenceParams::backend_buffer2 renders to. Bound and drawn to
|
||||
* ComposeSequenceParams::backend_buffer1 to "ping-pong" between them and various shaders.
|
||||
*/
|
||||
GLuint backend_attachment2;
|
||||
|
||||
/**
|
||||
* @brief OpenGL shader containing OpenColorIO shader information
|
||||
*/
|
||||
QOpenGLShaderProgram* ocio_shader;
|
||||
|
||||
/**
|
||||
* @brief OpenGL texture containing LUT obtained form OpenColorIO
|
||||
*/
|
||||
GLuint ocio_lut_texture;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Compose a frame of a given sequence
|
||||
*
|
||||
* For any given Sequence, this function will render the current frame indicated by Sequence::playhead. Will
|
||||
* automatically open and close clips (memory allocation and file handles) as necessary, communicate with the
|
||||
* Clip::cacher objects to retrieve upcoming frames and store them in memory, run Effect processing functions, and
|
||||
* finally composite all the currently active clips together into a final texture.
|
||||
*
|
||||
* Will sometimes render a frame incomplete or inaccurately, e.g. if a video file hadn't finished opening by the time
|
||||
* of the render or a clip's cacher didn't have the requested frame available at the time of the render. If so,
|
||||
* the `texture_failed` variable of `params` will be set to **TRUE**. Check this after calling compose_sequence() and
|
||||
* if it is **TRUE**, compose_sequence() should be called again later to attempt another render (unless the Sequence
|
||||
* is being played, in which case just play the next frame rather than redrawing an old frame).
|
||||
*
|
||||
* @param params
|
||||
*
|
||||
* A struct of parameters to use while rendering.
|
||||
*
|
||||
* @return A reference to the OpenGL texture resulting from the render. Will usually be equal to
|
||||
* ComposeSequenceParams::main_attachment unless it's rendering a nested sequence, in which case it'll be a reference
|
||||
* to one of the textures referenced by Clip::fbo. Can be used directly to draw the rendered frame.
|
||||
*/
|
||||
GLuint compose_sequence(ComposeSequenceParams ¶ms);
|
||||
|
||||
/**
|
||||
* @brief Convenience wrapper function for compose_sequence() to render audio
|
||||
*
|
||||
* Much of the functionality provided (and parameters required) by compose_sequence() is only useful/necessary for
|
||||
* video rendering. For audio rendering, this function is easier to handle and will correctly set up
|
||||
* compose_sequence() to render audio without the cumbersome effort of setting up a ComposeSequenceParams object.
|
||||
*
|
||||
* @param viewer
|
||||
*
|
||||
* The Viewer object calling this function
|
||||
*
|
||||
* @param seq
|
||||
*
|
||||
* The Sequence whose audio to render.
|
||||
*
|
||||
* @param playback_speed
|
||||
*
|
||||
* The current playback speed (controlled by Shuttle Left/Right)
|
||||
*
|
||||
* @param
|
||||
*
|
||||
* Whether to wait for media to open or simply fail if the media is not yet open. This should usually be **FALSE**.
|
||||
*/
|
||||
void compose_audio(Viewer* viewer, SequencePtr seq, int playback_speed, bool wait_for_mutexes);
|
||||
|
||||
/**
|
||||
* @brief Rescale a frame number between two frame rates
|
||||
*
|
||||
* Converts a frame number from one frame rate to its equivalent in another frame rate
|
||||
*
|
||||
* @param framenumber
|
||||
*
|
||||
* The frame number to convert
|
||||
*
|
||||
* @param source_frame_rate
|
||||
*
|
||||
* Frame rate that the frame number is currently in
|
||||
*
|
||||
* @param target_frame_rate
|
||||
*
|
||||
* Frame rate to convert to
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Rescaled frame number
|
||||
*/
|
||||
long rescale_frame_number(long framenumber, double source_frame_rate, double target_frame_rate);
|
||||
|
||||
/**
|
||||
* @brief Get timecode
|
||||
*
|
||||
* Get the current clip/media time from the Timeline playhead in seconds. For instance if the playhead was at the start
|
||||
* of a clip (whose in point wasn't trimmed), this would be 0.0 as it's the start of the clip/media;
|
||||
*
|
||||
* @param c
|
||||
*
|
||||
* Clip to get the timecode of
|
||||
*
|
||||
* @param playhead
|
||||
*
|
||||
* Sequence playhead to convert to a clip/media timecode
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Timecode in seconds
|
||||
*/
|
||||
double get_timecode(ClipPtr c, long playhead);
|
||||
|
||||
/**
|
||||
* @brief Convert playhead frame number to a clip frame number
|
||||
*
|
||||
* Converts a Timeline playhead to a the current clip's frame. Equivalent to
|
||||
* `PLAYHEAD - CLIP_TIMELINE_IN + CLIP_MEDIA_IN`. All keyframes are in clip frames.
|
||||
*
|
||||
* @param c
|
||||
*
|
||||
* The clip to get the current frame number of
|
||||
*
|
||||
* @param playhead
|
||||
*
|
||||
* The current Timeline frame number
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The curren frame number of the clip at `playhead`
|
||||
*/
|
||||
long playhead_to_clip_frame(ClipPtr c, long playhead);
|
||||
|
||||
/**
|
||||
* @brief Converts the playhead to clip seconds
|
||||
*
|
||||
* Get the current timecode at the playhead in terms of clip seconds.
|
||||
*
|
||||
* FIXME: Possible duplicate of get_timecode()? Will need to research this more.
|
||||
*
|
||||
* @param c
|
||||
*
|
||||
* Clip to return clip seconds of.
|
||||
*
|
||||
* @param playhead
|
||||
*
|
||||
* Current Timeline playhead to convert to clip seconds
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Clip time in seconds
|
||||
*/
|
||||
double playhead_to_clip_seconds(ClipPtr c, long playhead);
|
||||
|
||||
/**
|
||||
* @brief Convert seconds to FFmpeg timestamp
|
||||
*
|
||||
* Used for interaction with FFmpeg, converts seconds in a floating-point value to a timestamp in AVStream->time_base
|
||||
* units.
|
||||
*
|
||||
* @param c
|
||||
*
|
||||
* Clip to get timestamp of
|
||||
*
|
||||
* @param seconds
|
||||
*
|
||||
* Clip time in seconds
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* An FFmpeg-compatible timestamp in AVStream->time_base units.
|
||||
*/
|
||||
int64_t seconds_to_timestamp(ClipPtr c, double seconds);
|
||||
|
||||
/**
|
||||
* @brief Convert Timeline playhead to FFmpeg timestamp
|
||||
*
|
||||
* Used for interaction with FFmpeg, converts the Timeline playhead to a timestamp in AVStream->time_base
|
||||
* units.
|
||||
*
|
||||
* @param c
|
||||
*
|
||||
* Clip to get timestamp of
|
||||
*
|
||||
* @param playhead
|
||||
*
|
||||
* Timeline playhead to convert to a timestamp
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* An FFmpeg-compatible timestamp in AVStream->time_base units.
|
||||
*/
|
||||
int64_t playhead_to_timestamp(ClipPtr c, long playhead);
|
||||
|
||||
/**
|
||||
* @brief Close all open clips in a Sequence
|
||||
*
|
||||
* Closes any currently open clips on a Sequence and waits for them to close before returning. This may be slow as a
|
||||
* result on large Sequence objects. If a Clip is a nested Sequence, this function calls itself recursively on that
|
||||
* Sequence too.
|
||||
*
|
||||
* @param s
|
||||
*
|
||||
* The Sequence to close all clips on.
|
||||
*/
|
||||
void close_active_clips(SequencePtr s);
|
||||
|
||||
#endif // RENDERFUNCTIONS_H
|
||||
@@ -0,0 +1,350 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderthread.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QImage>
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#ifdef OLIVE_OCIO
|
||||
#include <OpenColorIO/OpenColorIO.h>
|
||||
namespace OCIO = OCIO_NAMESPACE;
|
||||
#endif
|
||||
|
||||
#include "rendering/renderfunctions.h"
|
||||
#include "project/sequence.h"
|
||||
|
||||
RenderThread::RenderThread()
|
||||
{
|
||||
front_buffer1 = 0;
|
||||
front_buffer2 = 0;
|
||||
front_texture1 = 0;
|
||||
front_texture2 = 0;
|
||||
gizmos = nullptr;
|
||||
share_ctx = nullptr;
|
||||
ctx = nullptr;
|
||||
blend_mode_program = nullptr;
|
||||
premultiply_program = nullptr;
|
||||
seq = nullptr;
|
||||
tex_width = -1;
|
||||
tex_height = -1;
|
||||
queued = false;
|
||||
texture_failed = false;
|
||||
running = true;
|
||||
|
||||
surface.create();
|
||||
}
|
||||
|
||||
RenderThread::~RenderThread() {
|
||||
surface.destroy();
|
||||
}
|
||||
|
||||
void RenderThread::run() {
|
||||
wait_lock_.lock();
|
||||
|
||||
while (running) {
|
||||
if (!queued) {
|
||||
wait_cond_.wait(&wait_lock_);
|
||||
}
|
||||
if (!running) {
|
||||
break;
|
||||
}
|
||||
queued = false;
|
||||
|
||||
if (share_ctx != nullptr) {
|
||||
if (ctx != nullptr) {
|
||||
ctx->makeCurrent(&surface);
|
||||
|
||||
// gen fbo
|
||||
if (front_buffer1 == 0) {
|
||||
// delete any existing framebuffers
|
||||
delete_fbo();
|
||||
|
||||
// create framebuffers
|
||||
ctx->functions()->glGenFramebuffers(1, &front_buffer1);
|
||||
ctx->functions()->glGenFramebuffers(1, &front_buffer2);
|
||||
ctx->functions()->glGenFramebuffers(1, &back_buffer_1);
|
||||
ctx->functions()->glGenFramebuffers(1, &back_buffer_2);
|
||||
}
|
||||
|
||||
// gen texture
|
||||
if (front_texture1 == 0 || tex_width != seq->width || tex_height != seq->height) {
|
||||
// cache texture size
|
||||
tex_width = seq->width;
|
||||
tex_height = seq->height;
|
||||
|
||||
// delete any existing textures
|
||||
delete_texture();
|
||||
|
||||
// create texture
|
||||
glGenTextures(1, &front_texture1);
|
||||
glGenTextures(1, &front_texture2);
|
||||
glGenTextures(1, &back_texture_1);
|
||||
glGenTextures(1, &back_texture_2);
|
||||
|
||||
GLuint textures[4] = { front_texture1, front_texture2, back_texture_1, back_texture_2 };
|
||||
GLuint fbos[4] = { front_buffer1, front_buffer2, back_buffer_1, back_buffer_2 };
|
||||
|
||||
for (int i=0;i<4;i++) {
|
||||
allocate_texture(textures[i], fbos[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (blend_mode_program == nullptr) {
|
||||
// create shader program to make blending modes work
|
||||
delete_shader_program();
|
||||
|
||||
blend_mode_program = new QOpenGLShaderProgram();
|
||||
blend_mode_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/internalshaders/common.vert");
|
||||
blend_mode_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/internalshaders/blending.frag");
|
||||
blend_mode_program->link();
|
||||
|
||||
premultiply_program = new QOpenGLShaderProgram();
|
||||
premultiply_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/internalshaders/common.vert");
|
||||
premultiply_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/internalshaders/premultiply.frag");
|
||||
premultiply_program->link();
|
||||
}
|
||||
|
||||
// draw frame
|
||||
paint();
|
||||
|
||||
front_buffer_switcher = !front_buffer_switcher;
|
||||
|
||||
emit ready();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete_ctx();
|
||||
|
||||
wait_lock_.unlock();
|
||||
}
|
||||
|
||||
QMutex *RenderThread::get_texture_mutex()
|
||||
{
|
||||
// return the mutex for the opposite texture being drawn to by the renderer
|
||||
return front_buffer_switcher ? &front_mutex2 : &front_mutex1;
|
||||
}
|
||||
|
||||
const GLuint &RenderThread::get_texture()
|
||||
{
|
||||
// return the opposite texture to the texture being drawn to by the renderer
|
||||
return front_buffer_switcher ? front_texture2 : front_texture1;
|
||||
}
|
||||
|
||||
void RenderThread::allocate_texture(GLuint tex, GLuint fbo)
|
||||
{
|
||||
// bind framebuffer for attaching
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
|
||||
// bind texture
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
// allocate storage for texture
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, GL_RGBA, seq->width, seq->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr
|
||||
);
|
||||
|
||||
// set texture filtering to bilinear
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
// attach texture to framebuffer
|
||||
ctx->functions()->glFramebufferTexture2D(
|
||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0
|
||||
);
|
||||
|
||||
// release texture
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// release framebuffer
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void RenderThread::set_up_ocio()
|
||||
{
|
||||
}
|
||||
|
||||
void RenderThread::paint() {
|
||||
// set up compose_sequence() parameters
|
||||
ComposeSequenceParams params;
|
||||
params.viewer = nullptr;
|
||||
params.ctx = ctx;
|
||||
params.seq = seq;
|
||||
params.video = true;
|
||||
params.texture_failed = false;
|
||||
params.gizmos = &gizmos;
|
||||
params.wait_for_mutexes = true;
|
||||
params.playback_speed = 1;
|
||||
params.blend_mode_program = blend_mode_program;
|
||||
params.premultiply_program = premultiply_program;
|
||||
params.backend_buffer1 = back_buffer_1;
|
||||
params.backend_buffer2 = back_buffer_2;
|
||||
params.backend_attachment1 = back_texture_1;
|
||||
params.backend_attachment2 = back_texture_2;
|
||||
params.main_buffer = front_buffer_switcher ? front_buffer1 : front_buffer2;
|
||||
params.main_attachment = front_buffer_switcher ? front_texture1 : front_texture2;
|
||||
params.main_buffer_mutex = front_buffer_switcher ? &front_mutex1 : &front_mutex2;
|
||||
|
||||
// bind framebuffer for drawing
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, params.main_buffer);
|
||||
|
||||
glLoadIdentity();
|
||||
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH);
|
||||
|
||||
gizmos = nullptr;
|
||||
|
||||
compose_sequence(params);
|
||||
|
||||
texture_failed = params.texture_failed;
|
||||
|
||||
if (!save_fn.isEmpty()) {
|
||||
if (texture_failed) {
|
||||
// texture failed, try again
|
||||
queued = true;
|
||||
} else {
|
||||
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, params.main_buffer);
|
||||
QImage img(tex_width, tex_height, QImage::Format_RGBA8888);
|
||||
glReadPixels(0, 0, tex_width, tex_height, GL_RGBA, GL_UNSIGNED_BYTE, img.bits());
|
||||
img.save(save_fn);
|
||||
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
save_fn = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (pixel_buffer != nullptr) {
|
||||
|
||||
// set main framebuffer to the current read buffer
|
||||
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, params.main_buffer);
|
||||
|
||||
// store pixels in buffer
|
||||
glReadPixels(0,
|
||||
0,
|
||||
pixel_buffer_linesize == 0 ? tex_width : pixel_buffer_linesize,
|
||||
tex_height,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
pixel_buffer);
|
||||
|
||||
// release current read buffer
|
||||
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
|
||||
pixel_buffer = nullptr;
|
||||
}
|
||||
|
||||
glDisable(GL_DEPTH);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
|
||||
// flush changes
|
||||
ctx->functions()->glFinish();
|
||||
|
||||
// release
|
||||
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void RenderThread::start_render(QOpenGLContext *share, SequencePtr s, const QString& save, GLvoid* pixels, int pixel_linesize, int idivider) {
|
||||
Q_UNUSED(idivider);
|
||||
|
||||
seq = s;
|
||||
|
||||
// stall any dependent actions
|
||||
texture_failed = true;
|
||||
|
||||
if (share != nullptr && (ctx == nullptr || ctx->shareContext() != share_ctx)) {
|
||||
share_ctx = share;
|
||||
delete_ctx();
|
||||
ctx = new QOpenGLContext();
|
||||
ctx->setFormat(share_ctx->format());
|
||||
ctx->setShareContext(share_ctx);
|
||||
ctx->create();
|
||||
ctx->moveToThread(this);
|
||||
}
|
||||
|
||||
save_fn = save;
|
||||
pixel_buffer = pixels;
|
||||
pixel_buffer_linesize = pixel_linesize;
|
||||
|
||||
queued = true;
|
||||
|
||||
wait_cond_.wakeAll();
|
||||
}
|
||||
|
||||
bool RenderThread::did_texture_fail() {
|
||||
return texture_failed;
|
||||
}
|
||||
|
||||
void RenderThread::cancel() {
|
||||
running = false;
|
||||
wait_cond_.wakeAll();
|
||||
wait();
|
||||
}
|
||||
|
||||
void RenderThread::delete_texture() {
|
||||
if (front_texture1 > 0) {
|
||||
GLuint tex[4] = {front_texture1, front_texture2, back_texture_1, back_texture_2};
|
||||
glDeleteTextures(3, tex);
|
||||
}
|
||||
front_texture1 = 0;
|
||||
front_texture2 = 0;
|
||||
back_texture_1 = 0;
|
||||
back_texture_2 = 0;
|
||||
}
|
||||
|
||||
void RenderThread::delete_fbo() {
|
||||
if (front_buffer1 > 0) {
|
||||
GLuint fbos[4] = {front_buffer1, front_buffer2, back_buffer_1, back_buffer_2};
|
||||
ctx->functions()->glDeleteFramebuffers(3, fbos);
|
||||
}
|
||||
front_buffer1 = 0;
|
||||
front_buffer2 = 0;
|
||||
back_buffer_1 = 0;
|
||||
back_buffer_2 = 0;
|
||||
}
|
||||
|
||||
void RenderThread::delete_shader_program() {
|
||||
if (blend_mode_program != nullptr) {
|
||||
delete blend_mode_program;
|
||||
delete premultiply_program;
|
||||
}
|
||||
blend_mode_program = nullptr;
|
||||
premultiply_program = nullptr;
|
||||
}
|
||||
|
||||
void RenderThread::delete_ctx() {
|
||||
if (ctx != nullptr) {
|
||||
delete_shader_program();
|
||||
delete_texture();
|
||||
delete_fbo();
|
||||
ctx->doneCurrent();
|
||||
delete ctx;
|
||||
}
|
||||
ctx = nullptr;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2019 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RENDERTHREAD_H
|
||||
#define RENDERTHREAD_H
|
||||
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
#include <QOffscreenSurface>
|
||||
#include <QOpenGLContext>
|
||||
#include <QOpenGLFramebufferObject>
|
||||
#include <QOpenGLShaderProgram>
|
||||
|
||||
#include "project/sequence.h"
|
||||
#include "project/effect.h"
|
||||
|
||||
// copied from source code to OCIODisplay
|
||||
const int LUT3D_EDGE_SIZE = 32;
|
||||
|
||||
// copied from source code to OCIODisplay, expanded from 3*LUT3D_EDGE_SIZE*LUT3D_EDGE_SIZE*LUT3D_EDGE_SIZE
|
||||
const int NUM_3D_ENTRIES = 98304;
|
||||
|
||||
class RenderThread : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderThread();
|
||||
~RenderThread();
|
||||
void run();
|
||||
|
||||
QMutex* get_texture_mutex();
|
||||
const GLuint& get_texture();
|
||||
|
||||
EffectPtr gizmos;
|
||||
void paint();
|
||||
void start_render(QOpenGLContext* share,
|
||||
SequencePtr s,
|
||||
const QString &save = nullptr,
|
||||
GLvoid *pixels = nullptr,
|
||||
int pixel_linesize = 0,
|
||||
int idivider = 0);
|
||||
bool did_texture_fail();
|
||||
void cancel();
|
||||
|
||||
|
||||
public slots:
|
||||
// cleanup functions
|
||||
void delete_ctx();
|
||||
signals:
|
||||
void ready();
|
||||
private:
|
||||
void allocate_texture(GLuint tex, GLuint fbo);
|
||||
|
||||
void set_up_ocio();
|
||||
void destroy_ocio();
|
||||
|
||||
// cleanup functions
|
||||
void delete_texture();
|
||||
void delete_fbo();
|
||||
void delete_shader_program();
|
||||
|
||||
GLuint front_buffer1;
|
||||
GLuint front_texture1;
|
||||
QMutex front_mutex1;
|
||||
|
||||
GLuint front_buffer2;
|
||||
GLuint front_texture2;
|
||||
QMutex front_mutex2;
|
||||
|
||||
bool front_buffer_switcher;
|
||||
|
||||
QWaitCondition wait_cond_;
|
||||
QMutex wait_lock_;
|
||||
|
||||
QWaitCondition main_thread_wait_cond_;
|
||||
QMutex main_thread_lock_;
|
||||
|
||||
QOffscreenSurface surface;
|
||||
QOpenGLContext* share_ctx;
|
||||
QOpenGLContext* ctx;
|
||||
QOpenGLShaderProgram* blend_mode_program;
|
||||
QOpenGLShaderProgram* premultiply_program;
|
||||
|
||||
GLuint back_buffer_1;
|
||||
GLuint back_buffer_2;
|
||||
GLuint back_texture_1;
|
||||
GLuint back_texture_2;
|
||||
|
||||
float ocio_lut_data[NUM_3D_ENTRIES];
|
||||
GLuint ocio_lut_texture;
|
||||
QOpenGLShaderProgram* ocio_shader;
|
||||
|
||||
SequencePtr seq;
|
||||
int divider;
|
||||
int tex_width;
|
||||
int tex_height;
|
||||
bool queued;
|
||||
bool texture_failed;
|
||||
bool running;
|
||||
QString save_fn;
|
||||
GLvoid *pixel_buffer;
|
||||
int pixel_buffer_linesize;
|
||||
};
|
||||
|
||||
#endif // RENDERTHREAD_H
|
||||
Reference in New Issue
Block a user