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:
1 parent
e264b8aaa7
commit
6542618927
11 files changed
+1168
-1
No files matched your search
@@ -80,6 +80,32 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
if (event->type() != QEvent::MouseButtonRelease) return false;
|
||||
Layout L = calcLayout(option, index);
|
||||
|
||||
if (L.isVideo) {
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
qint64 msgId = index.data(MsgIdRole).toLongLong();
|
||||
QString chId = index.data(MsgChannelIdRole).toString();
|
||||
|
||||
if (dlState == 2) { /* скачано — открыть плеер */
|
||||
QString fp = index.data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp) && m_videoOpenCb) {
|
||||
m_videoOpenCb(fp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (dlState == 1) return false; /* скачивается */
|
||||
|
||||
if (chId.isEmpty() || msgId <= 0) return false;
|
||||
struct attachment_dl_req* req = (struct attachment_dl_req*)
|
||||
u_malloc(sizeof(struct attachment_dl_req));
|
||||
if (!req) return false;
|
||||
memset(req, 0, sizeof(*req));
|
||||
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
|
||||
req->msg_id = msgId;
|
||||
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (L.isFile) {
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
qint64 msgId = index.data(MsgIdRole).toLongLong();
|
||||
@@ -345,6 +371,42 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
|
||||
return L;
|
||||
}
|
||||
|
||||
/* Video message detection */
|
||||
L.isVideo = (contentType == "video/mp4");
|
||||
if (L.isVideo) {
|
||||
int barW = 3, barPad = 4;
|
||||
int thW = 200, thH = 112; /* 16:9 превью */
|
||||
L.bubbleWidth = thW;
|
||||
L.totalHeight = qMax(thH, kAvatar) + kItemGap;
|
||||
|
||||
int contentEdge = L.isOutgoing
|
||||
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
|
||||
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
|
||||
int bx = L.isOutgoing ? (contentEdge - thW - barW - barPad) : contentEdge;
|
||||
L.bubbleRect = QRect(bx, 0, thW, thH);
|
||||
L.videoThumbRect = L.bubbleRect;
|
||||
L.mediaBarRect = QRect(L.isOutgoing ? bx + thW + 1 : bx - barW - 1, 0, barW, thH);
|
||||
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
int bd = index.data(MsgMediaBlocksDoneRole).toInt();
|
||||
int nb = index.data(MsgMediaNumBlocksRole).toInt();
|
||||
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0;
|
||||
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb;
|
||||
if (dlState == 1) { L.progressBarRect = QRect(bx, thH - 3, thW, 3); }
|
||||
|
||||
L.videoPlayBtnRect = QRect(bx + thW / 2 - 24, thH / 2 - 24, 48, 48);
|
||||
L.videoDurBadgeRect = QRect(bx + thW - 56, thH - 26, 48, 18);
|
||||
|
||||
if (!L.narrow) {
|
||||
int avatarY = L.bubbleRect.bottom() - kAvatar;
|
||||
if (!L.isOutgoing)
|
||||
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
|
||||
else
|
||||
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar);
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
/* File attachment detection */
|
||||
L.isFile = (contentType == "application/octet-stream");
|
||||
if (L.isFile) {
|
||||
@@ -899,6 +961,95 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
return;
|
||||
}
|
||||
|
||||
if (L.isVideo) {
|
||||
drawBubble(painter, L, option);
|
||||
if (!L.narrow) drawAvatar(painter, L, index);
|
||||
|
||||
QString filePath = index.data(MsgVoiceFileRole).toString();
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
QString thumbPath = filePath + ".jpg";
|
||||
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath clip;
|
||||
clip.addRoundedRect(L.videoThumbRect, kRadius, kRadius);
|
||||
painter->setClipPath(clip);
|
||||
|
||||
QPixmap thumb;
|
||||
if (!thumbPath.isEmpty() && QFile::exists(thumbPath))
|
||||
thumb.load(thumbPath);
|
||||
|
||||
if (!thumb.isNull()) {
|
||||
QPixmap scaled = thumb.scaled(L.videoThumbRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
int dx = (scaled.width() - L.videoThumbRect.width()) / 2;
|
||||
int dy = (scaled.height() - L.videoThumbRect.height()) / 2;
|
||||
painter->drawPixmap(L.videoThumbRect.topLeft(), scaled, QRect(dx, dy, L.videoThumbRect.width(), L.videoThumbRect.height()));
|
||||
} else {
|
||||
painter->fillRect(L.videoThumbRect, QColor(30, 30, 35));
|
||||
QFont iconFont = option.font;
|
||||
iconFont.setPointSize(iconFont.pointSize() + 10);
|
||||
painter->setFont(iconFont);
|
||||
painter->setPen(QColor(150, 150, 150));
|
||||
painter->drawText(L.videoThumbRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x8E\xAC"));
|
||||
}
|
||||
|
||||
/* play-оверлей */
|
||||
if (dlState == 2) {
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 110));
|
||||
painter->drawEllipse(L.videoPlayBtnRect);
|
||||
QPainterPath tri;
|
||||
int cx = L.videoPlayBtnRect.center().x() - L.videoPlayBtnRect.width() / 12;
|
||||
int cy = L.videoPlayBtnRect.center().y();
|
||||
int hs = L.videoPlayBtnRect.height() / 4;
|
||||
tri.moveTo(cx, cy - hs);
|
||||
tri.lineTo(cx + hs * 2, cy);
|
||||
tri.lineTo(cx, cy + hs);
|
||||
tri.closeSubpath();
|
||||
painter->setBrush(Qt::white);
|
||||
painter->drawPath(tri);
|
||||
}
|
||||
|
||||
/* duration-бейдж */
|
||||
QString durStr = index.data(MsgVoiceDurationRole).toString();
|
||||
if (!durStr.isEmpty()) {
|
||||
QFont durFont = option.font;
|
||||
durFont.setPointSize(durFont.pointSize() - 1);
|
||||
painter->setFont(durFont);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 160));
|
||||
painter->drawRoundedRect(L.videoDurBadgeRect, 5, 5);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(L.videoDurBadgeRect, Qt::AlignCenter, durStr);
|
||||
}
|
||||
painter->restore();
|
||||
|
||||
/* progress overlay (скачивание) */
|
||||
if (dlState == 1) {
|
||||
painter->setBrush(QColor(0, 0, 0, 140));
|
||||
painter->drawRect(L.videoThumbRect);
|
||||
QFont pfont = option.font;
|
||||
pfont.setPointSize(pfont.pointSize() + 2);
|
||||
painter->setFont(pfont);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(L.videoThumbRect, Qt::AlignCenter,
|
||||
QStringLiteral("%1%").arg((int)(L.progressFrac * 100)));
|
||||
}
|
||||
|
||||
/* media bar (левая кромка) */
|
||||
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80);
|
||||
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); }
|
||||
if (L.progressBarRect.isValid()) {
|
||||
painter->setBrush(QColor(0x44, 0x44, 0x44));
|
||||
painter->drawRoundedRect(L.progressBarRect, 2, 2);
|
||||
int pw = (int)(L.progressBarRect.width() * L.progressFrac);
|
||||
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); }
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (L.isFile) {
|
||||
drawBubble(painter, L, option);
|
||||
if (!L.narrow) drawAvatar(painter, L, index);
|
||||
|
||||
Reference in new issue
Block a user