Decode desktop image previews in bounded background jobs

This commit is contained in:
evgeny committed 2026-10-28 12:08:06 +01:00
1 parent 08e9b62e16
commit c6fdf52582
2 files changed
+51 -1

No files matched your search

+44
View File
@@ -358,6 +358,13 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
, m_recorder(recorder)
{
auto *layout = new QVBoxLayout(this);
m_imagePreviewTimer.setSingleShot(true);
connect(&m_imagePreviewTimer, &QTimer::timeout, this, &MessageList::prepareImagePreviews);
connect(m_model, &QAbstractItemModel::rowsInserted, this, [this]() { m_imagePreviewTimer.start(0); });
connect(m_model, &QAbstractItemModel::dataChanged, this,
[this](const QModelIndex&, const QModelIndex&, const QVector<int>& roles) {
if (roles.isEmpty() || roles.contains(MsgVoiceFileRole) || roles.contains(MsgContentTypeRole)) m_imagePreviewTimer.start(0);
});
if (auto* manager = AvatarManager::instance()) connect(manager, &AvatarManager::changed, this,
[this, manager](const QString& channel, quint64 owner) {
if (channel != m_currentChannelId && !isDmChannel(m_currentChannelId)) return;
@@ -1230,6 +1237,43 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) {
}
/* Общие превью для каналов/PM; не более двух decoder workers, без запуска из paint. */
void MessageList::prepareImagePreviews() {
for (int row = 0; row < m_model->rowCount(); ++row) {
auto* item = m_model->item(row);
if (!item->data(MsgContentTypeRole).toString().startsWith("image/")) continue;
const QString path = item->data(MsgVoiceFileRole).toString();
if (path.isEmpty() || item->data(MsgImagePreviewRole).isValid() || item->data(MsgImageErrorRole).toBool()) continue;
if (const auto* image = m_imagePreviews.object(path)) {
item->setData(*image, MsgImagePreviewRole); item->setData(image->isNull(), MsgImageErrorRole); continue;
}
if (m_imageJobs.contains(path)) continue;
m_imageJobs.insert(path); m_thumbnailPool.setMaxThreadCount(2);
QPointer<MessageList> self(this);
QObject* dispatcher = QCoreApplication::instance();
m_thumbnailPool.start([self, dispatcher, path]() {
QImageReader reader(path); reader.setAutoTransform(true);
const QSize size = reader.size();
QImage image;
if (size.isValid() && size.width() <= 16384 && size.height() <= 16384) {
reader.setScaledSize(size.scaled(600, 600, Qt::KeepAspectRatio)); image = reader.read();
}
if (image.isNull()) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "image preview failed path=%s dimensions=%dx%d error=%s",
qPrintable(path), size.width(), size.height(), qPrintable(reader.errorString()));
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "image preview ready path=%s dimensions=%dx%d", qPrintable(path), image.width(), image.height());
QMetaObject::invokeMethod(dispatcher, [self, path, image]() {
if (!self) return;
self->m_imageJobs.remove(path);
self->m_imagePreviews.insert(path, new QImage(image), qMax(1, (int)image.sizeInBytes()));
for (int r = 0; r < self->m_model->rowCount(); ++r) {
auto* current = self->m_model->item(r);
if (current->data(MsgVoiceFileRole).toString() != path) continue;
current->setData(image, MsgImagePreviewRole); current->setData(image.isNull(), MsgImageErrorRole);
}
}, Qt::QueuedConnection);
});
}
}
void MessageList::prepareVideoThumbnail(const QString& path) {
if (path.isEmpty() || m_thumbnailJobs.contains(path) || !QFile::exists(path) || QFile::exists(path + ".jpg")) return;
m_thumbnailPool.setMaxThreadCount(2);
+7 -1
View File
@@ -14,6 +14,8 @@
#include <QThreadPool>
#include <QJsonObject>
#include <QPointer>
#include <QCache>
#include <QImage>
#include "stream_sources.h"
class ChatView;
@@ -138,8 +140,12 @@ private:
QList<LiveStreamSource> m_streamSources; // Проверенный полный снимок выбранной группы.
RadioPanel* m_radioPanel = nullptr; /* верхняя панель устройств, VAD и PTT */
bool m_radioOn = false; // Локальный audio engine успешно включён для текущей группы.
QThreadPool m_thumbnailPool; // До двух FFmpeg workers; деструктор pool дожидается jobs.
QThreadPool m_thumbnailPool; // До двух media workers; деструктор pool дожидается jobs.
QSet<QString> m_thumbnailJobs; // Пути незавершённых jobs, без дублей.
QCache<QString, QImage> m_imagePreviews{32 * 1024 * 1024}; // Ограниченный кеш decoded-превью по неизменному пути.
QSet<QString> m_imageJobs; // Незавершённые image jobs; GUI изменяет, workers получают копию пути.
QTimer m_imagePreviewTimer; // Объединяет изменения модели до одного обхода видимой истории.
void prepareImagePreviews(); // Асинхронное декодирование локальных изображений и обновление актуальных строк модели.
QHash<QString, QJsonObject> m_feedback; // message_id hex → последний полный снимок реакций/опроса.
QPointer<QDialog> m_pollDialog; // Один диалог голосования; уничтожение owner безопасно для exec.
QString m_pollDialogMessage, m_pendingPoll; // Открытый message_id и ожидаемый запрос состояния.