You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
17 KiB
470 lines
17 KiB
#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(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<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); |
|
}
|
|
|