File indexing completed on 2024-12-01 04:29:21
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "thumbnailcache.hpp" 0007 #include "bin/projectclip.h" 0008 #include "bin/projectitemmodel.h" 0009 #include "core.h" 0010 #include "doc/kdenlivedoc.h" 0011 #include "project/projectmanager.h" 0012 #include <QDir> 0013 #include <QMutexLocker> 0014 #include <list> 0015 0016 std::unique_ptr<ThumbnailCache> ThumbnailCache::instance; 0017 std::once_flag ThumbnailCache::m_onceFlag; 0018 0019 class ThumbnailCache::Cache_t 0020 { 0021 public: 0022 Cache_t(int maxCost) 0023 : m_maxCost(maxCost) 0024 { 0025 } 0026 0027 bool contains(const QString &key) const { return m_cache.count(key) > 0; } 0028 0029 void remove(const QString &key) 0030 { 0031 if (!contains(key)) { 0032 return; 0033 } 0034 auto it = m_cache.at(key); 0035 m_currentCost -= (*it).second.second; 0036 // Need to erase reference to iterator before erasing what it points to. 0037 // Fixes BUG 463764. 0038 m_cache.erase(key); 0039 m_data.erase(it); 0040 } 0041 0042 void insert(const QString &key, const QImage &img, int cost) 0043 { 0044 if (cost > m_maxCost) { 0045 return; 0046 } 0047 m_data.push_front({key, {img, cost}}); 0048 auto it = m_data.begin(); 0049 m_cache[key] = it; 0050 m_currentCost += cost; 0051 while (m_currentCost > m_maxCost) { 0052 remove(m_data.back().first); 0053 } 0054 } 0055 0056 QImage get(const QString &key) 0057 { 0058 if (!contains(key)) { 0059 return QImage(); 0060 } 0061 // when a get operation occurs, we put the corresponding list item in front to remember last access 0062 std::pair<QString, std::pair<QImage, int>> data; 0063 auto it = m_cache.at(key); 0064 std::swap(data, (*it)); // take data out without copy 0065 QImage result = data.second.first; // a copy occurs here 0066 m_data.erase(it); // delete old iterator 0067 m_cache[key] = m_data.emplace(m_data.begin(), std::move(data)); // reinsert without copy and store iterator 0068 return result; 0069 } 0070 void clear() 0071 { 0072 m_data.clear(); 0073 m_cache.clear(); 0074 m_currentCost = 0; 0075 } 0076 bool checkIntegrity() const 0077 { 0078 if (m_data.size() != m_cache.size()) { 0079 // Cache is corrupted 0080 return false; 0081 } 0082 for (const auto &d : m_data) { 0083 if (!contains(d.first)) { 0084 return false; 0085 } 0086 } 0087 return true; 0088 } 0089 0090 protected: 0091 int m_maxCost; 0092 int m_currentCost{0}; 0093 0094 // The data is stored as (key,(image, cost)) in a std::list that serves as a 0095 // FIFO queue. If m_maxCost is exceeded, elements are removed from the 0096 // end of the list until the sum of the costs in the list is less than m_maxCost. 0097 std::list<std::pair<QString, std::pair<QImage, int>>> m_data; 0098 // m_cache keeps a mapping from the key to an iterator that represents the 0099 // item's location in m_data, like a pointer. 0100 std::unordered_map<QString, decltype(m_data.begin())> m_cache; 0101 }; 0102 0103 ThumbnailCache::ThumbnailCache() 0104 : m_volatileCache(new Cache_t(10000000)) 0105 { 0106 } 0107 0108 std::unique_ptr<ThumbnailCache> &ThumbnailCache::get() 0109 { 0110 std::call_once(m_onceFlag, [] { instance.reset(new ThumbnailCache()); }); 0111 return instance; 0112 } 0113 0114 bool ThumbnailCache::hasThumbnail(const QString &binId, int pos, bool volatileOnly) const 0115 { 0116 QMutexLocker locker(&m_mutex); 0117 bool ok = false; 0118 auto key = pos < 0 ? getAudioKey(binId, &ok).constFirst() : getKey(binId, pos, &ok); 0119 if (ok && m_volatileCache->contains(key)) { 0120 return true; 0121 } 0122 if (!ok || volatileOnly) { 0123 return false; 0124 } 0125 locker.unlock(); 0126 QDir thumbFolder = getDir(pos < 0, &ok); 0127 return ok && thumbFolder.exists(key); 0128 } 0129 0130 QImage ThumbnailCache::getAudioThumbnail(const QString &binId, bool volatileOnly) const 0131 { 0132 QMutexLocker locker(&m_mutex); 0133 bool ok = false; 0134 auto key = getAudioKey(binId, &ok).constFirst(); 0135 if (ok && m_volatileCache->contains(key)) { 0136 return m_volatileCache->get(key); 0137 } 0138 if (!ok || volatileOnly) { 0139 return QImage(); 0140 } 0141 QDir thumbFolder = getDir(true, &ok); 0142 if (ok && thumbFolder.exists(key)) { 0143 if (std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), -1) != m_storedOnDisk[binId].end()) { 0144 m_storedOnDisk[binId].push_back(-1); 0145 } 0146 locker.unlock(); 0147 return QImage(thumbFolder.absoluteFilePath(key)); 0148 } 0149 return QImage(); 0150 } 0151 0152 const QList<QUrl> ThumbnailCache::getAudioThumbPath(const QString &binId) const 0153 { 0154 bool ok = false; 0155 auto key = getAudioKey(binId, &ok); 0156 QDir thumbFolder = getDir(true, &ok); 0157 QList<QUrl> pathList; 0158 if (ok) { 0159 for (const QString &p : qAsConst(key)) { 0160 if (thumbFolder.exists(p)) { 0161 pathList << QUrl::fromLocalFile(thumbFolder.absoluteFilePath(p)); 0162 } 0163 } 0164 } 0165 return pathList; 0166 } 0167 0168 QImage ThumbnailCache::getThumbnail(QString hash, const QString &binId, int pos, bool volatileOnly) const 0169 { 0170 if (hash.isEmpty()) { 0171 return QImage(); 0172 } 0173 hash.append(QString("#%1.jpg").arg(pos)); 0174 QMutexLocker locker(&m_mutex); 0175 if (m_volatileCache->contains(hash)) { 0176 return m_volatileCache->get(hash); 0177 } 0178 if (volatileOnly) { 0179 return QImage(); 0180 } 0181 bool ok = false; 0182 QDir thumbFolder = getDir(false, &ok); 0183 if (ok && thumbFolder.exists(hash)) { 0184 if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() || 0185 std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) { 0186 m_storedOnDisk[binId].push_back(pos); 0187 } 0188 locker.unlock(); 0189 return QImage(thumbFolder.absoluteFilePath(hash)); 0190 } 0191 locker.unlock(); 0192 return QImage(); 0193 } 0194 0195 QImage ThumbnailCache::getThumbnail(const QString &binId, int pos, bool volatileOnly) const 0196 { 0197 QMutexLocker locker(&m_mutex); 0198 bool ok = false; 0199 auto key = getKey(binId, pos, &ok); 0200 if (ok && m_volatileCache->contains(key)) { 0201 return m_volatileCache->get(key); 0202 } 0203 if (!ok || volatileOnly) { 0204 return QImage(); 0205 } 0206 QDir thumbFolder = getDir(false, &ok); 0207 if (ok && thumbFolder.exists(key)) { 0208 if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() || 0209 std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) { 0210 m_storedOnDisk[binId].push_back(pos); 0211 } 0212 locker.unlock(); 0213 return QImage(thumbFolder.absoluteFilePath(key)); 0214 } 0215 return QImage(); 0216 } 0217 0218 void ThumbnailCache::storeThumbnail(const QString &binId, int pos, const QImage &img, bool persistent) 0219 { 0220 if (pCore->projectItemModel()->closing) { 0221 return; 0222 } 0223 QMutexLocker locker(&m_mutex); 0224 bool ok = false; 0225 const QString key = getKey(binId, pos, &ok); 0226 if (!ok) { 0227 return; 0228 } 0229 // if volatile cache also contains this entry, update it 0230 if (m_volatileCache->contains(key)) { 0231 m_volatileCache->remove(key); 0232 } else { 0233 m_storedVolatile[binId].push_back(pos); 0234 } 0235 m_volatileCache->insert(key, img, (int)img.sizeInBytes()); 0236 if (persistent) { 0237 QDir thumbFolder = getDir(false, &ok); 0238 if (ok) { 0239 if (m_storedOnDisk.find(binId) == m_storedOnDisk.end() || 0240 std::find(m_storedOnDisk[binId].begin(), m_storedOnDisk[binId].end(), pos) == m_storedOnDisk[binId].end()) { 0241 m_storedOnDisk[binId].push_back(pos); 0242 } 0243 locker.unlock(); 0244 if (!img.save(thumbFolder.absoluteFilePath(key))) { 0245 qDebug() << ".............\n!!!!!!!! ERROR SAVING THUMB in: " << thumbFolder.absoluteFilePath(key); 0246 } 0247 } 0248 } 0249 } 0250 0251 bool ThumbnailCache::checkIntegrity() const 0252 { 0253 return m_volatileCache->checkIntegrity(); 0254 } 0255 0256 void ThumbnailCache::saveCachedThumbs(const std::unordered_map<QString, std::vector<int>> &keys) 0257 { 0258 bool ok; 0259 QDir thumbFolder = getDir(false, &ok); 0260 if (!ok) { 0261 return; 0262 } 0263 QMutexLocker locker(&m_mutex); 0264 for (auto &key : keys) { 0265 bool ok; 0266 for (const auto &pos : key.second) { 0267 if (m_storedOnDisk.find(key.first) == m_storedOnDisk.end() || 0268 std::find(m_storedOnDisk[key.first].begin(), m_storedOnDisk[key.first].end(), pos) == m_storedOnDisk[key.first].end()) { 0269 const QString thumbKey = getKey(key.first, pos, &ok); 0270 if (!ok) { 0271 continue; 0272 } 0273 if (!thumbFolder.exists(thumbKey) && m_volatileCache->contains(thumbKey)) { 0274 QImage img = m_volatileCache->get(thumbKey); 0275 if (!img.save(thumbFolder.absoluteFilePath(thumbKey))) { 0276 qDebug() << "// Error writing thumbnails to " << thumbFolder.absolutePath(); 0277 break; 0278 } else { 0279 m_storedOnDisk[key.first].push_back(pos); 0280 } 0281 } 0282 } 0283 } 0284 } 0285 } 0286 0287 void ThumbnailCache::invalidateThumbsForClip(const QString &binId) 0288 { 0289 QMutexLocker locker(&m_mutex); 0290 if (m_storedVolatile.find(binId) != m_storedVolatile.end()) { 0291 bool ok = false; 0292 for (int pos : m_storedVolatile.at(binId)) { 0293 auto key = getKey(binId, pos, &ok); 0294 if (ok) { 0295 m_volatileCache->remove(key); 0296 } 0297 } 0298 m_storedVolatile.erase(binId); 0299 } 0300 bool ok = false; 0301 // Video thumbs 0302 QStringList files; 0303 if (m_storedOnDisk.find(binId) != m_storedOnDisk.end()) { 0304 // Remove persistent cache 0305 for (const auto &pos : m_storedOnDisk.at(binId)) { 0306 if (pos >= 0) { 0307 auto key = getKey(binId, pos, &ok); 0308 if (ok) { 0309 files << key; 0310 } 0311 } 0312 } 0313 m_storedOnDisk.erase(binId); 0314 } 0315 // Release mutex before deleting files 0316 locker.unlock(); 0317 if (!files.isEmpty()) { 0318 QDir thumbFolder = getDir(false, &ok); 0319 if (ok) { 0320 while (!files.isEmpty()) { 0321 thumbFolder.remove(files.takeFirst()); 0322 } 0323 } 0324 } 0325 } 0326 0327 void ThumbnailCache::clearCache() 0328 { 0329 QMutexLocker locker(&m_mutex); 0330 m_volatileCache->clear(); 0331 m_storedVolatile.clear(); 0332 m_storedOnDisk.clear(); 0333 } 0334 0335 // static 0336 QString ThumbnailCache::getKey(const QString &binId, int pos, bool *ok) 0337 { 0338 if (binId.isEmpty()) { 0339 *ok = false; 0340 return QString(); 0341 } 0342 auto binClip = pCore->projectItemModel()->getClipByBinID(binId); 0343 *ok = binClip != nullptr && binClip->statusReady(); 0344 if (!*ok) { 0345 return QString(); 0346 } 0347 return binClip->hashForThumbs() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".jpg"); 0348 } 0349 0350 // static 0351 QStringList ThumbnailCache::getAudioKey(const QString &binId, bool *ok) 0352 { 0353 auto binClip = pCore->projectItemModel()->getClipByBinID(binId); 0354 if (binClip == nullptr) { 0355 *ok = false; 0356 qWarning() << "[BUG] Could not find binClip for binId" << binId; 0357 return {}; 0358 } else { 0359 *ok = true; 0360 QString streams = binClip->getProducerProperty(QStringLiteral("kdenlive:active_streams")); 0361 if (streams == QString::number(INT_MAX)) { 0362 // activate all audio streams 0363 QList<int> streamIxes = binClip->audioStreams().keys(); 0364 if (streamIxes.size() > 1) { 0365 QStringList streamsList; 0366 for (const int st : qAsConst(streamIxes)) { 0367 streamsList << QString("%1_%2.png").arg(binClip->hash()).arg(st); 0368 } 0369 return streamsList; 0370 } 0371 } 0372 if (streams.size() < 2) { 0373 int audio = binClip->getProducerIntProperty(QStringLiteral("audio_index")); 0374 if (audio > -1) { 0375 return {QString("%1_%2.png").arg(binClip->hash()).arg(audio)}; 0376 } 0377 return {binClip->hash() + QStringLiteral(".png")}; 0378 } 0379 QStringList streamsList; 0380 QStringList streamIndexes = streams.split(QLatin1Char(';')); 0381 for (const QString &st : qAsConst(streamIndexes)) { 0382 streamsList << QString("%1_%2.png").arg(binClip->hash(), st); 0383 } 0384 return streamsList; 0385 } 0386 } 0387 0388 // static 0389 const QDir ThumbnailCache::getDir(bool audio, bool *ok) 0390 { 0391 return pCore->projectManager()->cacheDir(audio, ok); 0392 }