472 lines
17 KiB
C++
472 lines
17 KiB
C++
#include <QCoreApplication>
|
|
#include "videoplayer_window.h"
|
|
#include "videoplayer_engine.h"
|
|
#include "../../lib/debug_config.h"
|
|
|
|
#include <QSlider>
|
|
#include <QPushButton>
|
|
#include <QLabel>
|
|
#include <QHBoxLayout>
|
|
#include <QVBoxLayout>
|
|
#include <QPainter>
|
|
#include <QKeyEvent>
|
|
#include <QMouseEvent>
|
|
#include <QCloseEvent>
|
|
#include <QResizeEvent>
|
|
#include <QApplication>
|
|
#include <QScreen>
|
|
#include <QStyle>
|
|
#include <QProxyStyle>
|
|
|
|
/* ─── VideoRenderWidget ─── */
|
|
|
|
VideoRenderWidget::VideoRenderWidget(QWidget* parent) : QWidget(parent) {
|
|
setMouseTracking(true);
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
}
|
|
|
|
void VideoRenderWidget::setFrame(const QImage& img) {
|
|
m_frame = img;
|
|
update();
|
|
}
|
|
|
|
void VideoRenderWidget::clearFrame() {
|
|
m_frame = QImage();
|
|
update();
|
|
}
|
|
|
|
void VideoRenderWidget::paintEvent(QPaintEvent*) {
|
|
QPainter p(this);
|
|
p.fillRect(rect(), Qt::black);
|
|
if (m_frame.isNull()) return;
|
|
|
|
QSize sz = m_frame.size();
|
|
sz.scale(size(), Qt::KeepAspectRatio);
|
|
QRect dst((width() - sz.width()) / 2, (height() - sz.height()) / 2, sz.width(), sz.height());
|
|
p.drawImage(dst, m_frame);
|
|
}
|
|
|
|
/* ─── VideoPlayerWindow ─── */
|
|
|
|
static QString fmtTime(qint64 ms) {
|
|
qint64 s = ms / 1000;
|
|
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
|
|
}
|
|
|
|
static QPoint mouseGlobalPos(const QMouseEvent* e) {
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
return e->globalPosition().toPoint();
|
|
#else
|
|
return e->globalPos();
|
|
#endif
|
|
}
|
|
|
|
/* Слайдер перемотки: клик по любому месту шкалы = прыжок к позиции (нативно через стиль). */
|
|
class SeekSliderStyle : public QProxyStyle {
|
|
public:
|
|
using QProxyStyle::QProxyStyle;
|
|
|
|
int styleHint(StyleHint hint, const QStyleOption* opt, const QWidget* w,
|
|
QStyleHintReturn* ret) const override {
|
|
if (hint == QStyle::SH_Slider_AbsoluteSetButtons)
|
|
return (int)Qt::LeftButton;
|
|
return QProxyStyle::styleHint(hint, opt, w, ret);
|
|
}
|
|
};
|
|
|
|
VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
|
|
: QWidget(parent, Qt::Window | Qt::FramelessWindowHint)
|
|
{
|
|
setWindowTitle(QCoreApplication::translate("ChatGui", "Video"));
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
setMouseTracking(true);
|
|
|
|
m_engine = new VideoPlayerEngine(this);
|
|
|
|
m_render = new VideoRenderWidget(this);
|
|
m_render->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
|
|
m_controls = new QWidget(this);
|
|
m_controls->setObjectName("videoControls");
|
|
m_controls->setCursor(Qt::ArrowCursor);
|
|
m_controls->installEventFilter(this);
|
|
m_controls->setStyleSheet(
|
|
"#videoControls { background-color: rgba(0,0,0,0.65); }"
|
|
"QPushButton { background: transparent; color: white; border: none; font-size: 16px; }"
|
|
"QPushButton:hover { background: rgba(255,255,255,0.2); }"
|
|
"QLabel { color: white; font-size: 12px; }"
|
|
"QSlider::groove:horizontal { height: 4px; background: #444; }"
|
|
"QSlider::sub-page:horizontal { background: #2a9; }"
|
|
"QSlider::handle:horizontal { width: 12px; margin: -4px 0; background: white; border-radius: 6px; }");
|
|
|
|
m_playBtn = new QPushButton(QStringLiteral("⏸"), m_controls);
|
|
m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Pause"));
|
|
m_playBtn->setFixedWidth(40);
|
|
m_slider = new QSlider(Qt::Horizontal, m_controls);
|
|
m_slider->setStyle(new SeekSliderStyle());
|
|
m_slider->setRange(0, 0);
|
|
m_timeLabel = new QLabel(QStringLiteral("0:00 / 0:00"), m_controls);
|
|
|
|
auto* lay = new QHBoxLayout(m_controls);
|
|
lay->setContentsMargins(8, 3, 8, 6);
|
|
lay->setSpacing(6);
|
|
lay->addWidget(m_playBtn);
|
|
lay->addWidget(m_slider, 1);
|
|
lay->addWidget(m_timeLabel);
|
|
|
|
/* крестик закрытия — отдельно, справа вверху */
|
|
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
|
m_closeBtn->setToolTip(QCoreApplication::translate("ChatGui", "Close"));
|
|
m_closeBtn->setCursor(Qt::ArrowCursor);
|
|
m_closeBtn->setStyleSheet(
|
|
"QPushButton { background: rgba(0,0,0,0.5); color: white; border: none; "
|
|
"border-radius: 4px; font-size: 14px; }"
|
|
"QPushButton:hover { background: rgba(255,255,255,0.25); }");
|
|
|
|
m_hideTimer = new QTimer(this);
|
|
m_hideTimer->setSingleShot(true);
|
|
m_hideTimer->setInterval(2500);
|
|
connect(m_hideTimer, &QTimer::timeout, this, &VideoPlayerWindow::hideControls);
|
|
|
|
connect(m_playBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onPlayPause);
|
|
connect(m_closeBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onClose);
|
|
connect(m_slider, &QSlider::sliderPressed, this, &VideoPlayerWindow::onSliderPressed);
|
|
connect(m_slider, &QSlider::sliderReleased, this, &VideoPlayerWindow::onSliderReleased);
|
|
|
|
connect(m_engine, &VideoPlayerEngine::opened, this, &VideoPlayerWindow::onEngineOpened);
|
|
connect(m_engine, &VideoPlayerEngine::positionChanged, this, &VideoPlayerWindow::onPositionChanged);
|
|
connect(m_engine, &VideoPlayerEngine::stateChanged, this, &VideoPlayerWindow::onStateChanged);
|
|
connect(m_engine, &VideoPlayerEngine::error, this, &VideoPlayerWindow::onError);
|
|
connect(m_engine, &VideoPlayerEngine::framePresented, this, &VideoPlayerWindow::onFramePresented);
|
|
connect(m_engine, &VideoPlayerEngine::finished, this, &VideoPlayerWindow::onFinished);
|
|
|
|
resize(640, 360);
|
|
}
|
|
|
|
VideoPlayerWindow::~VideoPlayerWindow() = default;
|
|
|
|
void VideoPlayerWindow::playFile(const QString& path) {
|
|
m_render->clearFrame();
|
|
m_slider->setRange(0, 0);
|
|
m_timeLabel->setText(QStringLiteral("0:00 / 0:00"));
|
|
if (m_fullscreen) { showNormal(); m_fullscreen = false; setControlsVisible(true); unsetCursor(); }
|
|
show();
|
|
raise();
|
|
activateWindow();
|
|
m_engine->open(path);
|
|
}
|
|
|
|
void VideoPlayerWindow::onEngineOpened(int width, int height, qint64 durationMs, bool hasAudio) {
|
|
Q_UNUSED(hasAudio);
|
|
m_slider->setRange(0, (int)durationMs);
|
|
updateTimeLabels(0);
|
|
|
|
QScreen* scr = QApplication::primaryScreen();
|
|
QRect ag = scr ? scr->availableGeometry() : QRect(0, 0, 1280, 720);
|
|
QSize s = fitWindowSize(width, height);
|
|
|
|
if (width <= ag.width() && height <= ag.height()) {
|
|
/* видео помещается — натуральный размер по центру */
|
|
resize(s);
|
|
move(ag.center().x() - s.width() / 2, ag.center().y() - s.height() / 2);
|
|
} else {
|
|
/* видео больше экрана — fullscreen (нормальная геометрия для выхода уже задана) */
|
|
resize(s);
|
|
move(ag.center().x() - s.width() / 2, ag.center().y() - s.height() / 2);
|
|
showFullScreen();
|
|
m_fullscreen = true;
|
|
m_hideTimer->start();
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoWindow: %dx%d dur=%lldms size=%dx%d fullscreen=%d",
|
|
width, height, (long long)durationMs, s.width(), s.height(), m_fullscreen ? 1 : 0);
|
|
}
|
|
|
|
void VideoPlayerWindow::onPositionChanged(qint64 ms) {
|
|
if (!m_sliderDragging) {
|
|
m_slider->blockSignals(true);
|
|
m_slider->setValue((int)ms);
|
|
m_slider->blockSignals(false);
|
|
}
|
|
updateTimeLabels(ms);
|
|
}
|
|
|
|
void VideoPlayerWindow::onStateChanged(int state) {
|
|
VideoPlayerEngine::State s = (VideoPlayerEngine::State)state;
|
|
if (s == VideoPlayerEngine::Playing) {
|
|
m_playBtn->setText(QStringLiteral("⏸"));
|
|
m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Pause"));
|
|
} else if (s == VideoPlayerEngine::Paused || s == VideoPlayerEngine::Finished) {
|
|
m_playBtn->setText(QStringLiteral("▶"));
|
|
m_playBtn->setToolTip(QCoreApplication::translate("ChatGui", "Play"));
|
|
showControls();
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::onError(const QString& msg) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoWindow: %s", qPrintable(msg));
|
|
m_timeLabel->setText(msg);
|
|
showControls();
|
|
}
|
|
|
|
void VideoPlayerWindow::onFramePresented() {
|
|
m_render->setFrame(m_engine->currentFrame());
|
|
}
|
|
|
|
void VideoPlayerWindow::onSliderPressed() {
|
|
m_sliderDragging = true;
|
|
}
|
|
|
|
void VideoPlayerWindow::onSliderReleased() {
|
|
m_sliderDragging = false;
|
|
m_engine->seekMs(m_slider->value());
|
|
if (m_engine->state() == VideoPlayerEngine::Paused ||
|
|
m_engine->state() == VideoPlayerEngine::Finished) {
|
|
m_engine->play();
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::onPlayPause() {
|
|
if (m_engine->state() == VideoPlayerEngine::Playing) m_engine->pause();
|
|
else m_engine->togglePlayPause();
|
|
}
|
|
|
|
void VideoPlayerWindow::onClose() {
|
|
m_engine->close();
|
|
close();
|
|
}
|
|
|
|
void VideoPlayerWindow::onFinished() {
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoWindow: playback finished, closing");
|
|
onClose();
|
|
}
|
|
|
|
void VideoPlayerWindow::toggleFullscreen() {
|
|
if (m_fullscreen) {
|
|
showNormal();
|
|
m_fullscreen = false;
|
|
setControlsVisible(true);
|
|
unsetCursor();
|
|
} else {
|
|
showFullScreen();
|
|
m_fullscreen = true;
|
|
m_hideTimer->start();
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::setControlsVisible(bool visible) {
|
|
m_controls->setVisible(visible);
|
|
m_closeBtn->setVisible(visible);
|
|
}
|
|
|
|
void VideoPlayerWindow::showControls() {
|
|
setControlsVisible(true);
|
|
unsetCursor();
|
|
if (m_fullscreen) m_hideTimer->start();
|
|
}
|
|
|
|
void VideoPlayerWindow::hideControls() {
|
|
if (!m_fullscreen) return;
|
|
if (m_engine && (m_engine->state() == VideoPlayerEngine::Paused ||
|
|
m_engine->state() == VideoPlayerEngine::Finished)) return;
|
|
setControlsVisible(false);
|
|
setCursor(Qt::BlankCursor);
|
|
}
|
|
|
|
void VideoPlayerWindow::updateTimeLabels(qint64 posMs) {
|
|
qint64 dur = m_engine ? m_engine->durationMs() : 0;
|
|
m_timeLabel->setText(QString("%1 / %2").arg(fmtTime(posMs), fmtTime(dur)));
|
|
}
|
|
|
|
QSize VideoPlayerWindow::fitWindowSize(int vw, int vh) const {
|
|
if (vw <= 0 || vh <= 0) return QSize(640, 360);
|
|
QScreen* scr = QApplication::primaryScreen();
|
|
QSize avail(1280, 720);
|
|
if (scr) avail = scr->availableGeometry().size();
|
|
QSize s(vw, vh);
|
|
if (s.width() > avail.width() || s.height() > avail.height())
|
|
s.scale(avail, Qt::KeepAspectRatio);
|
|
return s;
|
|
}
|
|
|
|
void VideoPlayerWindow::keyPressEvent(QKeyEvent* e) {
|
|
if (e->key() == Qt::Key_Escape) {
|
|
if (m_fullscreen) { toggleFullscreen(); return; }
|
|
onClose();
|
|
return;
|
|
}
|
|
if (e->key() == Qt::Key_Return && (e->modifiers() & Qt::AltModifier)) {
|
|
toggleFullscreen();
|
|
return;
|
|
}
|
|
if (e->key() == Qt::Key_Space) {
|
|
onPlayPause();
|
|
return;
|
|
}
|
|
QWidget::keyPressEvent(e);
|
|
}
|
|
|
|
void VideoPlayerWindow::mousePressEvent(QMouseEvent* e) {
|
|
if (e->button() == Qt::LeftButton) {
|
|
if (m_fullscreen) {
|
|
/* клик в fullscreen — показать/скрыть контролы */
|
|
if (m_controls->isVisible()) hideControls();
|
|
else showControls();
|
|
return;
|
|
}
|
|
ResizeMode m = hitTestResize(e->pos());
|
|
if (m != ResizeNone) {
|
|
beginResize(m, mouseGlobalPos(e));
|
|
} else {
|
|
m_dragging = true;
|
|
m_dragStart = mouseGlobalPos(e);
|
|
m_windowStart = frameGeometry().topLeft();
|
|
}
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::mouseMoveEvent(QMouseEvent* e) {
|
|
if (m_fullscreen) {
|
|
showControls();
|
|
return;
|
|
}
|
|
if (m_resizeMode != ResizeNone && (e->buttons() & Qt::LeftButton)) {
|
|
applyResize(mouseGlobalPos(e));
|
|
return;
|
|
}
|
|
if (m_dragging && (e->buttons() & Qt::LeftButton)) {
|
|
move(m_windowStart + (mouseGlobalPos(e) - m_dragStart));
|
|
return;
|
|
}
|
|
if (!(e->buttons() & Qt::LeftButton))
|
|
updateResizeCursor(e->pos());
|
|
}
|
|
|
|
void VideoPlayerWindow::mouseReleaseEvent(QMouseEvent* e) {
|
|
if (e->button() == Qt::LeftButton) {
|
|
m_dragging = false;
|
|
m_resizeMode = ResizeNone;
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
|
if (e->button() == Qt::LeftButton) toggleFullscreen();
|
|
}
|
|
|
|
bool VideoPlayerWindow::eventFilter(QObject* obj, QEvent* ev) {
|
|
/* Резрез за нижний край: панель контролов перекрывает нижнюю кромку окна,
|
|
* поэтому маршрутизируем события из её пустых полей в логику resize. */
|
|
if (obj == m_controls && !m_fullscreen) {
|
|
QEvent::Type t = ev->type();
|
|
if (t == QEvent::MouseButtonPress || t == QEvent::MouseMove || t == QEvent::MouseButtonRelease) {
|
|
QMouseEvent* me = static_cast<QMouseEvent*>(ev);
|
|
QPoint winPos = m_controls->mapTo(this, me->pos());
|
|
ResizeMode m = hitTestResize(winPos);
|
|
if (m == ResizeNone) return QWidget::eventFilter(obj, ev);
|
|
|
|
QPoint gp = mouseGlobalPos(me);
|
|
if (t == QEvent::MouseButtonPress && me->button() == Qt::LeftButton) {
|
|
beginResize(m, gp);
|
|
return true;
|
|
}
|
|
if (t == QEvent::MouseMove && m_resizeMode != ResizeNone && (me->buttons() & Qt::LeftButton)) {
|
|
applyResize(gp);
|
|
return true;
|
|
}
|
|
if (t == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton) {
|
|
m_resizeMode = ResizeNone;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return QWidget::eventFilter(obj, ev);
|
|
}
|
|
|
|
void VideoPlayerWindow::closeEvent(QCloseEvent* e) {
|
|
m_engine->close();
|
|
emit closed();
|
|
QWidget::closeEvent(e);
|
|
}
|
|
|
|
void VideoPlayerWindow::resizeEvent(QResizeEvent* e) {
|
|
QWidget::resizeEvent(e);
|
|
m_render->setGeometry(rect());
|
|
int ch = 48;
|
|
m_controls->setGeometry(0, height() - ch, width(), ch);
|
|
m_controls->raise();
|
|
m_closeBtn->setGeometry(width() - 48, 8, 40, 32);
|
|
m_closeBtn->raise();
|
|
}
|
|
|
|
VideoPlayerWindow::ResizeMode VideoPlayerWindow::hitTestResize(const QPoint& pos) const {
|
|
const int M = 6;
|
|
int w = width(), h = height();
|
|
bool l = pos.x() <= M;
|
|
bool r = pos.x() >= w - M - 1;
|
|
bool t = pos.y() <= M;
|
|
bool b = pos.y() >= h - M - 1;
|
|
if (t && l) return ResizeTopLeft;
|
|
if (t && r) return ResizeTopRight;
|
|
if (b && l) return ResizeBottomLeft;
|
|
if (b && r) return ResizeBottomRight;
|
|
if (l) return ResizeLeft;
|
|
if (r) return ResizeRight;
|
|
if (t) return ResizeTop;
|
|
if (b) return ResizeBottom;
|
|
return ResizeNone;
|
|
}
|
|
|
|
void VideoPlayerWindow::updateResizeCursor(const QPoint& pos) {
|
|
switch (hitTestResize(pos)) {
|
|
case ResizeTopLeft: case ResizeBottomRight: setCursor(Qt::SizeFDiagCursor); break;
|
|
case ResizeTopRight: case ResizeBottomLeft: setCursor(Qt::SizeBDiagCursor); break;
|
|
case ResizeLeft: case ResizeRight: setCursor(Qt::SizeHorCursor); break;
|
|
case ResizeTop: case ResizeBottom: setCursor(Qt::SizeVerCursor); break;
|
|
default: unsetCursor(); break;
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWindow::beginResize(ResizeMode m, const QPoint& globalPos) {
|
|
m_resizeMode = m;
|
|
m_resizeStartGeom = geometry();
|
|
m_resizeStartGlobal = globalPos;
|
|
}
|
|
|
|
void VideoPlayerWindow::applyResize(const QPoint& globalPos) {
|
|
const int kMinW = 160;
|
|
QRect g = m_resizeStartGeom;
|
|
double aspect = (g.height() > 0) ? (double)g.width() / g.height() : 1.0;
|
|
int minH = qMax(1, qRound(kMinW / aspect));
|
|
int dx = globalPos.x() - m_resizeStartGlobal.x();
|
|
int dy = globalPos.y() - m_resizeStartGlobal.y();
|
|
|
|
int newW = g.width(), newH = g.height();
|
|
bool widthDriven = true;
|
|
switch (m_resizeMode) {
|
|
case ResizeRight: case ResizeTopRight: case ResizeBottomRight: newW = g.width() + dx; break;
|
|
case ResizeLeft: case ResizeTopLeft: case ResizeBottomLeft: newW = g.width() - dx; break;
|
|
case ResizeBottom: newH = g.height() + dy; widthDriven = false; break;
|
|
case ResizeTop: newH = g.height() - dy; widthDriven = false; break;
|
|
default: return;
|
|
}
|
|
if (widthDriven) newH = qRound(newW / aspect);
|
|
else newW = qRound(newH * aspect);
|
|
|
|
if (newW < kMinW) { newW = kMinW; newH = minH; }
|
|
if (newH < minH) { newH = minH; newW = kMinW; }
|
|
|
|
int cx = g.center().x(), cy = g.center().y();
|
|
int left = g.left(), top = g.top();
|
|
switch (m_resizeMode) {
|
|
case ResizeRight: left = g.left(); top = cy - newH / 2; break;
|
|
case ResizeLeft: left = g.right() - newW + 1; top = cy - newH / 2; break;
|
|
case ResizeBottom: left = cx - newW / 2; top = g.top(); break;
|
|
case ResizeTop: left = cx - newW / 2; top = g.bottom() - newH + 1; break;
|
|
case ResizeTopLeft: left = g.right() - newW + 1; top = g.bottom() - newH + 1; break;
|
|
case ResizeTopRight: left = g.left(); top = g.bottom() - newH + 1; break;
|
|
case ResizeBottomLeft: left = g.right() - newW + 1; top = g.top(); break;
|
|
case ResizeBottomRight: left = g.left(); top = g.top(); break;
|
|
default: return;
|
|
}
|
|
setGeometry(left, top, newW, newH);
|
|
}
|