chatgui: воспроизведение видео — FFmpeg-плеер + frameless окно

- videoplayer_engine: декод в worker-потоке (FFmpeg), A/V sync по аудио-курсору,
  seek с epoch-защитой от устаревших кадров
- videoplayer_window: окно без рамки, drag-перенос, dblclick/Alt+Enter fullscreen,
  Esc закрытие, timeline + play/pause/close, авто-скрытие контролов
- видео-баббл в списке: превью-кадр (jpg), play-оверлей, длительность, прогресс
- превью извлекается после скачивания (video_extract_thumbnail) в фоне
- mainwindow: синглтон-плеер, openVideoRequested → openVideo
- CMake: HAVE_FFMPEG для vibechat
This commit is contained in:
evgeny committed 2026-09-08 00:55:06 +03:00
1 parent e264b8aaa7
commit 6542618927
11 files changed
+1168 -1

No files matched your search

+58 -1
View File
@@ -24,12 +24,14 @@
#include <QFile>
#include <vector>
#include <memory>
#include <thread>
static std::vector<std::unique_ptr<LottieIcon>> s_icons;
extern "C" {
#include "../transport/gui_bridge.h"
#include "chat/chat_core.h"
#include "video/video.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
@@ -195,6 +197,42 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
}
item->setData(dlState, MsgFileDownloadStateRole);
}
if (ct == "video/mp4") {
QString raw = QString::fromUtf8(body);
int pipePos = raw.indexOf('|');
QString fname = (pipePos > 0) ? raw.left(pipePos) : raw;
item->setData(fname, MsgFileDisplayNameRole);
if (pipePos > 0) {
QString meta = raw.mid(pipePos + 1);
QStringList parts = meta.split('|');
if (parts.size() >= 1)
item->setData(parts[0].toULongLong(), MsgFileSizeRole);
if (parts.size() >= 3)
item->setData(parts[2].toUInt(), MsgMediaNumBlocksRole);
/* dur/w/h в хвосте: |dur=<ms>|w=<w>|h=<h> */
for (const QString& p : parts) {
if (p.startsWith("dur=")) {
qint64 durMs = p.mid(4).toLongLong();
if (durMs > 0) {
QString d = QString::number(durMs / 1000.0, 'f', 0) + "s";
item->setData(d, MsgVoiceDurationRole);
}
}
}
}
QJsonObject la = QJsonDocument::fromJson(localAttrs).object();
QString st = la.value("st").toString();
QString fp = la.value("fp").toString();
int dlState = 0;
if (st == "fl" && !fp.isEmpty()) {
dlState = 2;
QString fullPath = mediaDirBase + "/" + channelId + "/" + fp;
item->setData(fullPath, MsgVoiceFileRole);
}
item->setData(dlState, MsgFileDownloadStateRole);
}
}
static void setMsg(QStandardItem *item,
@@ -230,7 +268,11 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
layout->setSpacing(0);
m_view->setModel(m_model);
m_view->setItemDelegate(new MessageDelegate(this));
auto* msgDelegate = new MessageDelegate(this);
msgDelegate->setVideoOpenCallback([this](const QString& path) {
emit openVideoRequested(path);
});
m_view->setItemDelegate(msgDelegate);
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_view->setSelectionMode(QAbstractItemView::NoSelection);
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -702,4 +744,19 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) {
});
}
}
if (m.contentType == "video/mp4") {
QString fp = item->data(MsgVoiceFileRole).toString();
QString thumbPath = fp + ".jpg";
if (!fp.isEmpty() && QFile::exists(fp) && !QFile::exists(thumbPath)) {
std::thread([this, fp, thumbPath]() {
int rc = video_extract_thumbnail(fp.toUtf8().constData(),
thumbPath.toUtf8().constData(), 320);
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "MessageList: thumbnail %s rc=%d",
qPrintable(thumbPath), rc);
QMetaObject::invokeMethod(this, [this]() { m_view->viewport()->update(); },
Qt::QueuedConnection);
}).detach();
}
}
}