#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')); } /* Слайдер перемотки: клик по любому месту шкалы = прыжок к позиции (драг кружка сохраняется). */ class SeekSlider : public QSlider { public: using QSlider::QSlider; protected: void mousePressEvent(QMouseEvent* e) override { if (e->button() == Qt::LeftButton) { setSliderDown(true); int v = QStyle::sliderValueFromPosition(minimum(), maximum(), (int)e->position().x(), width()); setValue(v); emit sliderPressed(); e->accept(); } else { QSlider::mousePressEvent(e); } } }; VideoPlayerWindow::VideoPlayerWindow(QWidget* parent) : QWidget(parent, Qt::Window | Qt::FramelessWindowHint) { setWindowTitle(QStringLiteral("Video")); setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); setStyleSheet("background-color: black;"); 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_slider = new SeekSlider(Qt::Horizontal, m_controls); 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); m_snapTimer = new QTimer(this); m_snapTimer->setSingleShot(true); m_snapTimer->setInterval(150); connect(m_snapTimer, &QTimer::timeout, this, &VideoPlayerWindow::snapToAspect); 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); 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); m_videoAspect = (height > 0) ? (double)width / height : 0.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::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) { m_resizeEdges = edgesForMode(m); if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges); } else { if (windowHandle()) windowHandle()->startSystemMove(); } } } void VideoPlayerWindow::mouseMoveEvent(QMouseEvent* e) { if (m_fullscreen) { showControls(); return; } if (!(e->buttons() & Qt::LeftButton)) updateResizeCursor(e->pos()); } void VideoPlayerWindow::mouseDoubleClickEvent(QMouseEvent* e) { if (e->button() == Qt::LeftButton) toggleFullscreen(); } bool VideoPlayerWindow::eventFilter(QObject* obj, QEvent* ev) { /* Резрез за нижний край: панель контролов перекрывает нижнюю кромку окна, * поэтому маршрутизируем press из её пустых полей в нативный resize. */ if (obj == m_controls && !m_fullscreen && ev->type() == QEvent::MouseButtonPress) { QMouseEvent* me = static_cast(ev); if (me->button() == Qt::LeftButton) { QPoint winPos = m_controls->mapTo(this, me->pos()); ResizeMode m = hitTestResize(winPos); if (m != ResizeNone) { m_resizeEdges = edgesForMode(m); if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges); 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() - 40, 8, 32, 32); m_closeBtn->raise(); /* во время нативного resize снэпаем пропорцию после окончания жеста */ if (m_resizeEdges != 0) m_snapTimer->start(); } 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; } } Qt::Edges VideoPlayerWindow::edgesForMode(ResizeMode m) { switch (m) { case ResizeLeft: return Qt::LeftEdge; case ResizeRight: return Qt::RightEdge; case ResizeTop: return Qt::TopEdge; case ResizeBottom: return Qt::BottomEdge; case ResizeTopLeft: return Qt::TopEdge | Qt::LeftEdge; case ResizeTopRight: return Qt::TopEdge | Qt::RightEdge; case ResizeBottomLeft: return Qt::BottomEdge | Qt::LeftEdge; case ResizeBottomRight: return Qt::BottomEdge | Qt::RightEdge; default: return {}; } } void VideoPlayerWindow::snapToAspect() { if (m_videoAspect <= 0.0 || m_resizeEdges == 0) return; Qt::Edges edges = m_resizeEdges; m_resizeEdges = {}; /* сброс до resize(), чтобы снэп не ре-триггерил сам себя */ int w = width(), h = height(); if (w <= 0 || h <= 0) return; bool verticalOnly = !(edges & (Qt::LeftEdge | Qt::RightEdge)); if (verticalOnly) { int newW = qRound(h * m_videoAspect); if (qAbs(newW - w) > 1) resize(newW, h); } else { int newH = qRound(w / m_videoAspect); if (qAbs(newH - h) > 1) resize(w, newH); } }