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

#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);
}
}