Commit b392b15f authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Initial commit to transition from jobmanager's QtConcurrent model to more...

Initial commit to transition from jobmanager's QtConcurrent model to more flexible and simpler QRunnable
parent 534cea43
......@@ -48,6 +48,7 @@ AbstractProjectItem::AbstractProjectItem(PROJECTITEMTYPE type, QString id, const
, m_usage(0)
, m_rating(0)
, m_clipStatus(FileStatus::StatusReady)
, m_clipJobProgress(0)
, m_itemType(type)
, m_lock(QReadWriteLock::Recursive)
, m_isCurrent(false)
......@@ -183,6 +184,9 @@ QVariant AbstractProjectItem::getData(DataType type) const
break;
case JobStatus:
if (itemType() == ClipItem) {
if (m_clipJobProgress > 0) {
return QVariant::fromValue(JobManagerStatus::Running);
}
auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId());
if (jobIds.empty()) {
jobIds = pCore->jobManager()->getFinishedJobsIds(clipId());
......@@ -196,6 +200,7 @@ QVariant AbstractProjectItem::getData(DataType type) const
break;
case JobProgress:
if (itemType() == ClipItem) {
return m_clipJobProgress;
auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId());
if (jobIds.size() > 0) {
data = QVariant(pCore->jobManager()->getJobProgressForClip(jobIds[0], clipId()));
......
......@@ -219,6 +219,7 @@ protected:
uint m_rating;
QString m_tags;
FileStatus::ClipStatus m_clipStatus;
int m_clipJobProgress;
PROJECTITEMTYPE m_itemType;
......
......@@ -79,6 +79,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <QUrl>
#include <QVBoxLayout>
#include <utility>
#include <jobs/audiolevelstask.h>
/**
* @class BinItemDelegate
......@@ -4058,9 +4059,6 @@ void Bin::reloadAllProducers(bool reloadThumbs)
ThumbnailCache::get()->invalidateThumbsForClip(clip->clipId());
}
pCore->jobManager()->startJob<ThumbJob>({clip->clipId()}, jobId, QString(), -1, true, true);
if (KdenliveSettings::audiothumbnails()) {
pCore->jobManager()->startJob<AudioThumbJob>({clip->clipId()}, jobId, QString());
}
}
}
}
......@@ -4074,7 +4072,7 @@ void Bin::checkAudioThumbs()
for (const auto &clip : qAsConst(clipList)) {
ClipType::ProducerType type = clip->clipType();
if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Playlist || type == ClipType::Unknown) {
pCore->jobManager()->startJob<AudioThumbJob>({clip->clipId()}, -1, QString());
AudioLevelsTask::start(clip->clipId(), this, false);
}
}
}
......
......@@ -30,6 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "jobs/audiothumbjob.hpp"
#include "jobs/jobmanager.h"
#include "jobs/loadjob.hpp"
#include "jobs/audiolevelstask.h"
#include "jobs/thumbjob.hpp"
#include "jobs/cachejob.hpp"
#include "kdenlivesettings.h"
......@@ -120,6 +121,9 @@ ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::share
setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
connectEffectStack();
if (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly) {
AudioLevelsTask::start(m_binId, this, false);
}
}
// static
......@@ -413,9 +417,6 @@ void ProjectClip::reloadProducer(bool refreshOnly, bool isProxy, bool forceAudio
if (forceAudioReload || (!isProxy && hashChanged)) {
discardAudioThumb();
}
if (KdenliveSettings::audiothumbnails()) {
emit pCore->jobManager()->startJob<AudioThumbJob>({clipId()}, loadJob, QString());
}
}
}
}
......@@ -529,6 +530,7 @@ bool ProjectClip::setProducer(std::shared_ptr<Mlt::Producer> producer, bool repl
getFileHash();
// set parent again (some info need to be stored in producer)
updateParent(parentItem().lock());
AudioLevelsTask::start(m_binId, this, false);
if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) {
QList<std::shared_ptr<ProjectClip>> clipList;
......@@ -1778,7 +1780,16 @@ const QVector <uint8_t> ProjectClip::audioFrameCache(int stream)
return audioLevels;
}
}
QString key = QString("%1:%2").arg(m_binId).arg(stream);
QString key = QString("_kdenlive:audio%1").arg(stream);
if (m_masterProducer->get_data(key.toUtf8().constData())) {
const QVector <uint8_t> audioData = *static_cast<QVector<uint8_t> *>(m_masterProducer->get_data(key.toUtf8().constData()));
return audioData;
} else {
qDebug()<<"=== AUDIO NOT FOUND ";
}
return QVector <uint8_t>();
/*QString key = QString("%1:%2").arg(m_binId).arg(stream);
QByteArray audioData;
if (pCore->audioThumbCache.find(key, &audioData)) {
if (audioData != QByteArray("-")) {
......@@ -1806,7 +1817,7 @@ const QVector <uint8_t> ProjectClip::audioFrameCache(int stream)
st << audioLevels;
pCore->audioThumbCache.insert(key, audioData);
}
return audioLevels;
return audioLevels;*/
}
void ProjectClip::setClipStatus(FileStatus::ClipStatus status)
......@@ -2010,3 +2021,15 @@ void ProjectClip::updateTimelineOnReload()
}
}
}
void ProjectClip::audioJobProgress(int progress)
{
if (progress == 100) {
m_clipJobProgress = 0;
} else {
m_clipJobProgress = progress;
}
if (auto ptr = m_model.lock()) {
std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(m_binId, AbstractProjectItem::JobProgress);
}
}
......@@ -141,13 +141,6 @@ public:
void setThumbnail(const QImage &);
QPixmap thumbnail(int width, int height);
/** @brief Sets the MLT producer associated with this clip
* @param producer The producer
* @param replaceProducer If true, we replace existing producer with this one
* @returns true if producer was changed
* . */
bool setProducer(std::shared_ptr<Mlt::Producer> producer, bool replaceProducer);
/** @brief Returns this clip's producer. */
std::shared_ptr<Mlt::Producer> thumbProducer() override;
......@@ -278,6 +271,7 @@ public slots:
void updateAudioThumbnail();
/** @brief Delete the proxy file */
void deleteProxy();
void audioJobProgress(int progress);
/**
* Imports effect from a given producer
......@@ -286,6 +280,13 @@ public slots:
*/
void importEffects(const std::shared_ptr<Mlt::Producer> &producer, QString originalDecimalPoint = QString());
/** @brief Sets the MLT producer associated with this clip
* @param producer The producer
* @param replaceProducer If true, we replace existing producer with this one
* @returns true if producer was changed
* . */
bool setProducer(std::shared_ptr<Mlt::Producer> producer, bool replaceProducer);
private:
/** @brief Generate and store file hash if not available. */
const QString getFileHash();
......
......@@ -40,6 +40,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "projectfolder.h"
#include "projectsubclip.h"
#include "lib/localeHandling.h"
#include "jobs/audiolevelstask.h"
#include "jobs/cliploadtask.h"
#include "xml/xml.hpp"
#include <KLocalizedString>
......@@ -602,6 +604,9 @@ bool ProjectItemModel::requestBinClipDeletion(const std::shared_ptr<AbstractProj
binId = ptr->clipId();
}
bool isSubClip = clip->itemType() == AbstractProjectItem::SubClipItem;
if (!isSubClip) {
AudioLevelsTask::cancel(clip->clipId());
}
clip->selfSoftDelete(undo, redo);
int id = clip->getId();
Fun operation = removeItem_lambda(id);
......@@ -721,14 +726,10 @@ bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &descrip
ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast<ProjectItemModel>(shared_from_this()));
bool res = addItem(new_clip, parentId, undo, redo);
if (res) {
int loadJob = emit pCore->jobManager()->startJob<LoadJob>({id}, -1, QString(), description, std::bind(readyCallBack, id));
ClipLoadTask::start(id, description, this);
//int loadJob = emit pCore->jobManager()->startJob<LoadJob>({id}, -1, QString(), description, std::bind(readyCallBack, id));
int loadJob = -1;
emit pCore->jobManager()->startJob<ThumbJob>({id}, loadJob, QString(), 0, true);
ClipType::ProducerType type = new_clip->clipType();
if (KdenliveSettings::audiothumbnails()) {
if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Playlist || type == ClipType::Unknown) {
emit pCore->jobManager()->startJob<AudioThumbJob>({id}, loadJob, QString());
}
}
}
return res;
}
......@@ -762,9 +763,6 @@ bool ProjectItemModel::requestAddBinClip(QString &id, const std::shared_ptr<Mlt:
if (new_clip->statusReady() || new_clip->sourceExists()) {
int blocking = pCore->jobManager()->getBlockingJobId(id, AbstractClipJob::LOADJOB);
emit pCore->jobManager()->startJob<ThumbJob>({id}, blocking, QString(), -1, true);
if (KdenliveSettings::audiothumbnails()) {
emit pCore->jobManager()->startJob<AudioThumbJob>({id}, blocking, QString());
}
}
}
return res;
......@@ -1050,6 +1048,7 @@ void ProjectItemModel::loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tract
}
i.value()->set("_kdenlive_processed", 1);
requestAddBinClip(newId, std::move(i.value()), parentId, undo, redo);
qApp->processEvents();
binIdCorresp[QString::number(i.key())] = newId;
}
}
......
......@@ -49,6 +49,7 @@ Core::Core()
, m_thumbProfile(nullptr)
, m_capture(new MediaCapture(this))
{
clipJobPool.setMaxThreadCount(2);
}
void Core::prepareShutdown()
......@@ -1036,4 +1037,3 @@ void Core::updateMasterZones()
m_mainWindow->getCurrentTimeline()->controller()->updateMasterZones(m_mainWindow->getCurrentTimeline()->controller()->getModel()->getMasterEffectZones());
}
}
......@@ -20,6 +20,7 @@ the Free Software Foundation, either version 3 of the License, or
#include <QUrl>
#include <memory>
#include <QPoint>
#include <QThreadPool>
#include <QTextEdit>
#include <KSharedDataCache>
#include <unordered_set>
......@@ -71,6 +72,9 @@ public:
Core &operator=(Core &&) = delete;
~Core() override;
/* @brief The thread job pool for clip jobs, allowing to set a max number of concurrent jobs */
QThreadPool clipJobPool;
/**
* @brief Setup the basics of the application, in particular the connection
......
......@@ -2,6 +2,8 @@ set(kdenlive_SRCS
${kdenlive_SRCS}
jobs/abstractclipjob.cpp
jobs/audiothumbjob.cpp
jobs/audiolevelstask.cpp
jobs/cliploadtask.cpp
jobs/jobmanager.cpp
jobs/cachejob.cpp
jobs/loadjob.cpp
......
/*
* Copyright (c) 2013-2021 Meltytech, LLC
* Copyright (c) 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
*
* 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 "audiolevelstask.h"
#include "core.h"
#include "bin/projectitemmodel.h"
#include "bin/projectclip.h"
#include "audio/audioStreamInfo.h"
#include <QString>
#include <QVariantList>
#include <QImage>
#include <QList>
#include <QRgb>
#include <QThreadPool>
#include <QMutex>
#include <QTime>
#include <QFile>
#include <QElapsedTimer>
static QList<AudioLevelsTask*> tasksList;
static QMutex tasksListMutex;
static void deleteQVariantList(QVector <uint8_t>* list)
{
delete list;
}
AudioLevelsTask::AudioLevelsTask(const QString &clipId, QObject* object)
: QRunnable()
, m_cid(clipId)
, m_object(object)
, m_isCanceled(false)
, m_isForce(false)
{
setAutoDelete(true);
}
const QString AudioLevelsTask::clipId() const
{
return m_cid;
}
AudioLevelsTask::~AudioLevelsTask()
{
}
void AudioLevelsTask::cancel(const QString cid)
{
QMutexLocker lk(&tasksListMutex);
// See if there is already a task for this MLT service and resource.
foreach (AudioLevelsTask* t, tasksList) {
if (t->clipId() == cid) {
// If so, then just add ourselves to be notified upon completion.
t->m_isCanceled = true;
break;
}
}
}
void AudioLevelsTask::start(const QString cid, QObject* object, bool force)
{
AudioLevelsTask* task = new AudioLevelsTask(cid, object);
tasksListMutex.lock();
// See if there is already a task for this MLT service and resource.
foreach (AudioLevelsTask* t, tasksList) {
if (*t == *task) {
// If so, then just add ourselves to be notified upon completion.
delete task;
task = 0;
break;
}
}
if (task) {
// Otherwise, start a new audio levels generation thread.
task->m_isForce = force;
tasksList << task;
pCore->clipJobPool.start(task, 3);
}
tasksListMutex.unlock();
}
void AudioLevelsTask::closeAll()
{
// Tell all of the audio levels tasks to stop.
tasksListMutex.lock();
while (!tasksList.isEmpty()) {
AudioLevelsTask* task = tasksList.first();
task->m_isCanceled = true;
tasksList.removeFirst();
}
tasksListMutex.unlock();
}
bool AudioLevelsTask::operator==(AudioLevelsTask &b)
{
return m_cid == b.clipId();
}
void AudioLevelsTask::cleanup()
{
tasksListMutex.lock();
for (int i = 0; i < tasksList.size(); ++i) {
if (*tasksList[i] == *this) {
tasksList.removeAt(i);
break;
}
}
tasksListMutex.unlock();
}
void AudioLevelsTask::run()
{
// 2 channels interleaved of uchar values
if (m_isCanceled) {
cleanup();
return;
}
auto binClip = pCore->projectItemModel()->getClipByBinID(m_cid);
if (binClip == nullptr) {
// Clip was deleted
cleanup();
return;
}
if (binClip->audioChannels() == 0 || binClip->audioThumbCreated()) {
// nothing to do
cleanup();
return;
}
std::shared_ptr<Mlt::Producer> producer = binClip->originalProducer();
if ((producer == nullptr) || !producer->is_valid()) {
/*m_errorMessage.append(i18n("Audio thumbs: cannot open project file %1", m_binClip->url()));
m_done = true;
m_successful = false;*/
cleanup();
return;
}
int lengthInFrames = producer->get_length(); // Multiply this if we want more than 1 sample per frame
if (lengthInFrames == INT_MAX) {
// This is a broken file or live feed, don't attempt to generate audio thumbnails
cleanup();
return;
}
int frequency = binClip->audioInfo()->samplingRate();
frequency = frequency <= 0 ? 48000 : frequency;
int channels = binClip->audioInfo()->channels();
channels = channels <= 0 ? 2 : channels;
QMap <int, QString> streams = binClip->audioInfo()->streams();
QMap <int, int> audioChannels = binClip->audioInfo()->streamChannels();
QMapIterator<int, QString> st(streams);
while (st.hasNext() && !m_isCanceled) {
st.next();
int stream = st.key();
if (audioChannels.contains(stream)) {
channels = audioChannels.value(stream);
}
// Generate one thumb per stream
QString cachePath = binClip->getAudioThumbPath(stream);
QVector <uint8_t> mltLevels;
qDebug()<<" TESTING AUDIO CACHE : "<<cachePath;
if (!m_isForce && QFile::exists(cachePath)) {
// Audio thumb already exists
QImage image(cachePath);
if (!m_isCanceled && !image.isNull()) {
// convert cached image
int n = image.width() * image.height();
for (int i = 0; n > 1 && i < n; i++) {
QRgb p = image.pixel(i / 2, i % channels);
mltLevels << qRed(p);
mltLevels << qGreen(p);
mltLevels << qBlue(p);
mltLevels << qAlpha(p);
}
if (mltLevels.size() > 0) {
QVector <uint8_t>* levelsCopy = new QVector <uint8_t>(mltLevels);
producer->lock();
QString key = QString("_kdenlive:audio%1").arg(stream);
producer->set(key.toUtf8().constData(), levelsCopy, 0, (mlt_destructor) deleteQVariantList);
producer->unlock();
qDebug()<<"=== FINISHED PRODUCING AUDIO FOR: "<<key<<", SIZE: "<<levelsCopy->size();
QMetaObject::invokeMethod(m_object, "updateAudioThumbnail");
cleanup();
return;
}
}
}
QString service = producer->get("mlt_service");
if (service == QLatin1String("avformat-novalidate")) {
service = QStringLiteral("avformat");
} else if (service.startsWith(QLatin1String("xml"))) {
service = QStringLiteral("xml-nogl");
}
QScopedPointer<Mlt::Producer> audioProducer(new Mlt::Producer(*producer->profile(), service.toUtf8().constData(), producer->get("resource")));
if (!audioProducer->is_valid()) {
//m_errorMessage.append(i18n("Audio thumbs: cannot open file %1", producer->get("resource")));
cleanup();
return;
}
audioProducer->set("video_index", "-1");
Mlt::Filter chans(*producer->profile(), "audiochannels");
Mlt::Filter converter(*producer->profile(), "audioconvert");
Mlt::Filter levels(*producer->profile(), "audiolevel");
audioProducer->attach(chans);
audioProducer->attach(converter);
audioProducer->attach(levels);
double framesPerSecond = audioProducer->get_fps();
mlt_audio_format audioFormat = mlt_audio_s16;
QStringList keys;
keys.reserve(channels);
for (int i = 0; i < channels; i++) {
keys << "meta.media.audio_level." + QString::number(i);
}
uint maxLevel = 1;
int last_val = 0;
QElapsedTimer updateTime;
updateTime.start();
for (int z = 0; z < lengthInFrames && !m_isCanceled; ++z) {
int val = int(100.0 * z / lengthInFrames);
if (last_val != val) {
QMetaObject::invokeMethod(m_object, "audioJobProgress", Q_ARG(const int, val));
last_val = val;
}
QScopedPointer<Mlt::Frame> mltFrame(audioProducer->get_frame());
if ((mltFrame != nullptr) && mltFrame->is_valid() && (mltFrame->get_int("test_audio") == 0)) {
int samples = mlt_audio_calculate_frame_samples(float(framesPerSecond), frequency, z);
mltFrame->get_audio(audioFormat, frequency, channels, samples);
for (int channel = 0; channel < channels; ++channel) {
uint lev = 256 * qMin(mltFrame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0);
mltLevels << lev;
//double lev = mltFrame->get_double(keys.at(channel).toUtf8().constData());
//mltLevels << lev;
maxLevel = qMax(lev, maxLevel);
}
} else if (!mltLevels.isEmpty()) {
for (int channel = 0; channel < channels; channel++) {
mltLevels << mltLevels.last();
}
}
// Incrementally update the audio levels every 3 seconds.
if (updateTime.elapsed() > 3000 && !m_isCanceled) {
updateTime.restart();
QVector <uint8_t>* levelsCopy = new QVector <uint8_t>(mltLevels);
producer->lock();
QString key = QString("_kdenlive:audio%1").arg(stream);
producer->set(key.toUtf8().constData(), levelsCopy, 0, (mlt_destructor) deleteQVariantList);
producer->unlock();
qDebug()<<"=== FINISHED PRODUCING AUDIO FOR: "<<key<<", SIZE: "<<levelsCopy->size();
QMetaObject::invokeMethod(m_object, "updateAudioThumbnail");
}
}
/*// Normalize
for (double &v : mltLevels) {
m_audioLevels << uchar(255 * v / maxLevel);
}*/
if (!m_isCanceled) {
QMetaObject::invokeMethod(m_object, "audioJobProgress", Q_ARG(const int, 100));
if (mltLevels.size() > 0 && !m_isCanceled) {
QVector <uint8_t>* levelsCopy = new QVector <uint8_t>(mltLevels);
producer->lock();
QString key = QString("_kdenlive:audio%1").arg(stream);
producer->set(key.toUtf8().constData(), levelsCopy, 0, (mlt_destructor) deleteQVariantList);
producer->unlock();
qDebug()<<"=== FINISHED PRODUCING AUDIO FOR: "<<key<<", SIZE: "<<levelsCopy->size();
QMetaObject::invokeMethod(m_object, "updateAudioThumbnail");
// Put into an image for caching.
int count = mltLevels.size();
QImage image((count + 3) / 4 / channels, channels, QImage::Format_ARGB32);
int n = image.width() * image.height();
for (int i = 0; i < n; i ++) {
QRgb p;
if ((4*i + 3) < count) {
p = qRgba(mltLevels.at(4*i), mltLevels.at(4*i+1), mltLevels.at(4*i+2), mltLevels.at(4*i+3));
} else {
int last = mltLevels.last();
int r = (4*i+0) < count? mltLevels.at(4*i+0) : last;
int g = (4*i+1) < count? mltLevels.at(4*i+1) : last;
int b = (4*i+2) < count? mltLevels.at(4*i+2) : last;
int a = last;
p = qRgba(r, g, b, a);
}
image.setPixel(i / 2, i % channels, p);
}
image.save(cachePath);
}
}
}
cleanup();
}
/*
* Copyright (c) 2013-2018 Meltytech, LLC
* Copyright (c) 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
* Author: Dan Dennedy <dan@dennedy.org>
*
* 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 AUDIOLEVELSTASK_H
#define AUDIOLEVELSTASK_H
#include <QRunnable>
#include <QObject>
class AudioLevelsTask : public QRunnable
{
public:
AudioLevelsTask(const QString &clipId, QObject* object);
virtual ~AudioLevelsTask();