#include "videoplayer_window.h" #include "videoplayer_engine.h" #include "../../lib/debug_config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* ─── 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(QStringLiteral("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(QStringLiteral("Пауза")); 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(QStringLiteral("Закрыть")); 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(QStringLiteral("Пауза")); } else if (s == VideoPlayerEngine::Paused || s == VideoPlayerEngine::Finished) { m_playBtn->setText(QStringLiteral("▶")); m_playBtn->setToolTip(QStringLiteral("Играть")); 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(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); }