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.
429 lines
15 KiB
429 lines
15 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 <QWindow> |
|
|
|
/* ─── 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<QMouseEvent*>(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); |
|
} |
|
}
|
|
|