diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index fa1b94a8f..9ac55035c 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -2401,18 +2401,11 @@ float64 OverlayWidget::playbackControlsCurrentVolume() { void OverlayWidget::switchToPip() { const auto document = _doc; const auto msgId = _msgid; - _pip = std::make_unique(_streamed->instance.shared(), [=] { + _pip = std::make_unique(this, _streamed->instance.shared(), [=] { showDocument(_doc, Auth().data().message(msgId), {}, true); + }, [=] { + _pip = nullptr; }); - _pip->show(); - _pip->events( - ) | rpl::filter([=](not_null e) { - return (e->type() == QEvent::Close); - }) | rpl::start_with_next([=] { - crl::on_main(_pip.get(), [=] { - _pip = nullptr; - }); - }, _pip->lifetime()); close(); } diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index e2ab09fda..50dd834a9 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_document.h" +#include "media/streaming/media_streaming_utility.h" #include "core/application.h" #include "window/window_controller.h" #include "styles/style_window.h" @@ -32,194 +33,6 @@ constexpr auto kPipLoaderPriority = 2; : QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight); } -} // namespace - -Pip::Pip( - std::shared_ptr document, - FnMut closeAndContinue) -: _instance(document, [=] { waitingAnimationCallback(); }) -, _closeAndContinue(std::move(closeAndContinue)) { - setWindowFlags(Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); - setupSize(); - setupStreaming(); -} - -void Pip::setupSize() { - _size = style::ConvertScale(_instance.info().video.size); - if (_size.isEmpty()) { - _size = QSize(st::windowMinWidth, st::windowMinHeight); - } - - const auto widgetScreen = [&](auto &&widget) -> QScreen* { - if (auto handle = widget ? widget->windowHandle() : nullptr) { - return handle->screen(); - } - return nullptr; - }; - const auto window = Core::App().activeWindow() - ? Core::App().activeWindow()->widget().get() - : nullptr; - const auto activeWindowScreen = widgetScreen(window); - const auto myScreen = widgetScreen(this); - if (activeWindowScreen && myScreen && myScreen != activeWindowScreen) { - windowHandle()->setScreen(activeWindowScreen); - } - const auto screen = activeWindowScreen - ? activeWindowScreen - : QGuiApplication::primaryScreen(); - const auto available = screen->geometry(); - const auto fit = QSize(available.width() / 2, available.height() / 2); - if (_size.width() > fit.width() || _size.height() > fit.height()) { - const auto byHeight = (fit.width() * _size.height() > fit.height() * _size.width()); - _size = byHeight - ? QSize(_size.width() * fit.height() / _size.height(), fit.height()) - : QSize(fit.width(), _size.height() * fit.width() / _size.width()); - } - const auto skip = st::pipBorderSkip; - setGeometry({ - available.x() + skip, - available.y() + skip, - _size.width(), - _size.height() - }); -} - -void Pip::setupStreaming() { - _instance.setPriority(kPipLoaderPriority); - _instance.lockPlayer(); - - _instance.player().updates( - ) | rpl::start_with_next_error([=](Streaming::Update &&update) { - handleStreamingUpdate(std::move(update)); - }, [=](Streaming::Error &&error) { - handleStreamingError(std::move(error)); - }, _instance.lifetime()); -} - -void Pip::handleStreamingUpdate(Streaming::Update &&update) { - using namespace Streaming; - - update.data.match([&](Information &update) { - setupSize(); - }, [&](const PreloadedVideo &update) { - //updatePlaybackState(); - }, [&](const UpdateVideo &update) { - this->update(); - Core::App().updateNonIdle(); - //updatePlaybackState(); - }, [&](const PreloadedAudio &update) { - //updatePlaybackState(); - }, [&](const UpdateAudio &update) { - //updatePlaybackState(); - }, [&](WaitingForData) { - }, [&](MutedByOther) { - }, [&](Finished) { - //updatePlaybackState(); - close(); - }); -} - -void Pip::handleStreamingError(Streaming::Error &&error) { - close(); -} - -void Pip::paintEvent(QPaintEvent *e) { - QPainter p(this); - - const auto rect = QRect(QPoint(), size()); - const auto image = videoFrameForDirectPaint(); - const auto rotation = _instance.info().video.rotation; - const auto rotated = [](QRect rect, int rotation) { - switch (rotation) { - case 0: return rect; - case 90: return QRect( - rect.y(), - -rect.x() - rect.width(), - rect.height(), - rect.width()); - case 180: return QRect( - -rect.x() - rect.width(), - -rect.y() - rect.height(), - rect.width(), - rect.height()); - case 270: return QRect( - -rect.y() - rect.height(), - rect.x(), - rect.height(), - rect.width()); - } - Unexpected("Rotation in OverlayWidget::paintTransformedVideoFrame"); - }; - - PainterHighQualityEnabler hq(p); - if (rotation) { - p.save(); - p.rotate(rotation); - } - p.drawImage(rotated(rect, rotation), image); - if (rotation) { - p.restore(); - } - if (_instance.player().ready()) { - _instance.markFrameShown(); - } -} - -void Pip::mousePressEvent(QMouseEvent *e) { - if (e->button() != Qt::LeftButton) { - return; - } - _pressPoint = e->globalPos(); -} - -void Pip::mouseReleaseEvent(QMouseEvent *e) { - if (e->button() != Qt::LeftButton || !base::take(_pressPoint)) { - return; - } else if (!base::take(_dragStartPosition)) { - playbackPauseResume(); - } else { - finishDrag(e->globalPos()); - } -} - -void Pip::mouseMoveEvent(QMouseEvent *e) { - if (!_pressPoint) { - return; - } - const auto point = e->globalPos(); - const auto distance = QApplication::startDragDistance(); - if (!_dragStartPosition - && (point - *_pressPoint).manhattanLength() > distance) { - _dragStartPosition = pos(); - } - if (_dragStartPosition) { - updatePosition(point); - } -} - -void Pip::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Space) { - playbackPauseResume(); - } else if (e->key() == Qt::Key_Escape) { - crl::on_main(this, [=] { - _closeAndContinue(); - }); - } -} - -void Pip::playbackPauseResume() { - if (_instance.player().finished() || !_instance.player().active()) { - //restartAtSeekPosition(0); - } else if (_instance.player().paused()) { - _instance.resume(); - //updatePlaybackState(); - //playbackPauseMusic(); - } else { - _instance.pause(); - //updatePlaybackState(); - } -} - [[nodiscard]] QPoint ClampToEdges(QRect screen, QRect inner) { const auto skip = st::pipBorderSkip; const auto area = st::pipBorderSnapArea; @@ -248,7 +61,218 @@ void Pip::playbackPauseResume() { return inner.topLeft() + QPoint(shiftx, shifty); } -void Pip::updatePosition(QPoint point) { +} // namespace + +PipPanel::PipPanel( + QWidget *parent, + Fn paint) +: _parent(parent) +, _paint(std::move(paint)) { + setWindowFlags(Qt::Tool + | Qt::WindowStaysOnTopHint + | Qt::FramelessWindowHint); + setAttribute(Qt::WA_ShowWithoutActivating); + resize(0, 0); +} + +void PipPanel::setAspectRatio(QSize ratio) { + if (_ratio == ratio) { + return; + } + _ratio = ratio; + if (_ratio.isEmpty()) { + _ratio = QSize(1, 1); + } + if (!size().isEmpty()) { + setPosition(countPosition()); + } +} + +void PipPanel::setPosition(Position position) { + if (!position.screen.isEmpty()) { + for (const auto screen : QApplication::screens()) { + if (screen->geometry() == position.screen) { + setPositionOnScreen(position, screen->availableGeometry()); + return; + } + } + } + setPositionDefault(); +} + +PipPanel::Position PipPanel::countPosition() const { + const auto screen = windowHandle() ? windowHandle()->screen() : nullptr; + if (!screen) { + return Position(); + } + auto result = Position(); + result.screen = screen->geometry(); + result.geometry = geometry(); + const auto available = screen->availableGeometry(); + const auto skip = st::pipBorderSkip; + const auto left = result.geometry.x(); + const auto right = left + result.geometry.width(); + const auto top = result.geometry.y(); + const auto bottom = top + result.geometry.height(); + if (left == available.x()) { + result.attached |= RectPart::Left; + } else if (right == available.x() + available.width()) { + result.attached |= RectPart::Right; + } else if (left == available.x() + skip) { + result.snapped |= RectPart::Left; + } else if (right == available.x() + available.width() - skip) { + result.snapped |= RectPart::Right; + } + if (top == available.y()) { + result.attached |= RectPart::Top; + } else if (bottom == available.y() + available.height()) { + result.attached |= RectPart::Bottom; + } else if (top == available.y() + skip) { + result.snapped |= RectPart::Top; + } else if (bottom == available.y() + available.height() - skip) { + result.snapped |= RectPart::Bottom; + } + return result; +} + +void PipPanel::setPositionDefault() { + const auto widgetScreen = [&](auto &&widget) -> QScreen* { + if (auto handle = widget ? widget->windowHandle() : nullptr) { + return handle->screen(); + } + return nullptr; + }; + const auto parentScreen = widgetScreen(_parent); + const auto myScreen = widgetScreen(this); + if (parentScreen && myScreen && myScreen != parentScreen) { + windowHandle()->setScreen(parentScreen); + } + const auto screen = parentScreen + ? parentScreen + : QGuiApplication::primaryScreen(); + auto position = Position(); + position.snapped = RectPart::Top | RectPart::Left; + position.screen = screen->geometry(); + position.geometry = QRect(0, 0, st::pipDefaultSize, st::pipDefaultSize); + setPositionOnScreen(position, screen->availableGeometry()); +} + +void PipPanel::setPositionOnScreen(Position position, QRect available) { + const auto screen = available; + const auto requestedSize = position.geometry.size(); + const auto max = std::max(requestedSize.width(), requestedSize.height()); + + // Apply aspect ratio. + const auto scaled = (_ratio.width() > _ratio.height()) + ? QSize(max, max * _ratio.height() / _ratio.width()) + : QSize(max * _ratio.width() / _ratio.height(), max); + + // At least one side should not be greater than half of screen size. + const auto byHeight = (scaled.width() * screen.height()) + > (scaled.height() * screen.width()); + const auto fit = QSize(screen.width() / 2, screen.height() / 2); + const auto normalized = (byHeight && scaled.height() > fit.height()) + ? QSize( + fit.height() * scaled.width() / scaled.height(), + fit.height()) + : (!byHeight && scaled.width() > fit.width()) + ? QSize(fit.width(), fit.width() * scaled.height() / scaled.width()) + : scaled; + + // Apply minimal size. + const auto size = QSize( + std::max(normalized.width(), st::pipMinimalSize), + std::max(normalized.height(), st::pipMinimalSize)); + + // Apply left-right screen borders. + const auto skip = st::pipBorderSkip; + const auto inner = screen.marginsRemoved({ skip, skip, skip, skip }); + auto geometry = QRect(position.geometry.topLeft(), size); + if ((position.attached & RectPart::Left) + || (geometry.x() < screen.x())) { + geometry.moveLeft(screen.x()); + } else if ((position.attached & RectPart::Right) + || (geometry.x() + geometry.width() > screen.x() + screen.width())) { + geometry.moveLeft(screen.x() + screen.width() - geometry.width()); + } else if (position.snapped & RectPart::Left) { + geometry.moveLeft(inner.x()); + } else if (position.snapped & RectPart::Right) { + geometry.moveLeft(inner.x() + inner.width() - geometry.width()); + } + + // Apply top-bottom screen borders. + if ((position.attached & RectPart::Top) || (geometry.y() < screen.y())) { + geometry.moveTop(screen.y()); + } else if ((position.attached & RectPart::Bottom) + || (geometry.y() + geometry.height() + > screen.y() + screen.height())) { + geometry.moveTop( + screen.y() + screen.height() - geometry.height()); + } else if (position.snapped & RectPart::Top) { + geometry.moveTop(inner.y()); + } else if (position.snapped & RectPart::Bottom) { + geometry.moveTop(inner.y() + inner.height() - geometry.height()); + } + + setGeometry(geometry); + _attached = position.attached; + update(); +} + +void PipPanel::paintEvent(QPaintEvent *e) { + QPainter p(this); + + auto request = FrameRequest(); + request.outer = size(); + request.corners = RectPart(0) + | ((_attached & (RectPart::Left | RectPart::Top)) + ? RectPart(0) + : RectPart::TopLeft) + | ((_attached & (RectPart::Top | RectPart::Right)) + ? RectPart(0) + : RectPart::TopRight) + | ((_attached & (RectPart::Right | RectPart::Bottom)) + ? RectPart(0) + : RectPart::BottomRight) + | ((_attached & (RectPart::Bottom | RectPart::Left)) + ? RectPart(0) + : RectPart::BottomLeft); + _paint(p, request); +} + +void PipPanel::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + _pressPoint = e->globalPos(); +} + +void PipPanel::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton || !base::take(_pressPoint)) { + return; + } else if (!base::take(_dragStartPosition)) { + //playbackPauseResume(); + } else { + finishDrag(e->globalPos()); + } +} + +void PipPanel::mouseMoveEvent(QMouseEvent *e) { + if (!_pressPoint) { + return; + } + const auto point = e->globalPos(); + const auto distance = QApplication::startDragDistance(); + if (!_dragStartPosition + && (point - *_pressPoint).manhattanLength() > distance) { + _dragStartPosition = pos(); + } + if (_dragStartPosition) { + updatePosition(point); + } +} + +void PipPanel::updatePosition(QPoint point) { Expects(_dragStartPosition.has_value()); const auto position = *_dragStartPosition + (point - *_pressPoint); @@ -262,7 +286,7 @@ void Pip::updatePosition(QPoint point) { } } -void Pip::finishDrag(QPoint point) { +void PipPanel::finishDrag(QPoint point) { const auto screen = ScreenFromPosition(point); const auto position = pos(); const auto clamped = [&] { @@ -288,7 +312,7 @@ void Pip::finishDrag(QPoint point) { } } -void Pip::updatePositionAnimated() { +void PipPanel::updatePositionAnimated() { const auto progress = _positionAnimation.value(1.); if (!_positionAnimation.animating()) { move(_positionAnimationTo); @@ -299,7 +323,7 @@ void Pip::updatePositionAnimated() { move((from + (to - from) * progress).toPoint()); } -void Pip::moveAnimated(QPoint to) { +void PipPanel::moveAnimated(QPoint to) { if (_positionAnimation.animating() && _positionAnimationTo == to) { return; } @@ -314,14 +338,124 @@ void Pip::moveAnimated(QPoint to) { anim::easeOutCirc); } -QImage Pip::videoFrame() const { - return _instance.player().ready() - ? _instance.frame(Streaming::FrameRequest()) - : _instance.info().video.cover; +void PipPanel::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Space) { + //playbackPauseResume(); + } else if (e->key() == Qt::Key_Escape) { + //crl::on_main(this, [=] { + // _closeAndContinue(); + //}); + } } -QImage Pip::videoFrameForDirectPaint() const { - const auto result = videoFrame(); +Pip::Pip( + QWidget *parent, + std::shared_ptr document, + FnMut closeAndContinue, + FnMut destroy) +: _instance(document, [=] { waitingAnimationCallback(); }) +, _panel(parent, [=](QPainter &p, const FrameRequest &request) { + paint(p, request); +}) +, _closeAndContinue(std::move(closeAndContinue)) +, _destroy(std::move(destroy)) { + setupPanel(); + setupStreaming(); +} + +void Pip::setupPanel() { + const auto size = style::ConvertScale(_instance.info().video.size); + if (size.isEmpty()) { + _panel.setAspectRatio(QSize(1, 1)); + } else { + _panel.setAspectRatio(size); + } + _panel.setPosition(PipPanel::Position()); + //_panel.events( + //) | rpl::filter([=](not_null e) { + // return (e->type() == QEvent::WindowActivate); + //}) | rpl::start_with_next([=](not_null e) { + // int a = 0; + //}, _panel.lifetime()); + _panel.show(); +} + +void Pip::setupStreaming() { + _instance.setPriority(kPipLoaderPriority); + _instance.lockPlayer(); + + _instance.player().updates( + ) | rpl::start_with_next_error([=](Streaming::Update &&update) { + handleStreamingUpdate(std::move(update)); + }, [=](Streaming::Error &&error) { + handleStreamingError(std::move(error)); + }, _instance.lifetime()); +} + +void Pip::paint(QPainter &p, const FrameRequest &request) { + const auto image = videoFrameForDirectPaint(request); + p.drawImage(0, 0, image); + if (_instance.player().ready()) { + _instance.markFrameShown(); + } +} + +void Pip::handleStreamingUpdate(Streaming::Update &&update) { + using namespace Streaming; + + update.data.match([&](Information &update) { + _panel.setAspectRatio(update.video.size); + }, [&](const PreloadedVideo &update) { + //updatePlaybackState(); + }, [&](const UpdateVideo &update) { + _panel.update(); + Core::App().updateNonIdle(); + //updatePlaybackState(); + }, [&](const PreloadedAudio &update) { + //updatePlaybackState(); + }, [&](const UpdateAudio &update) { + //updatePlaybackState(); + }, [&](WaitingForData) { + }, [&](MutedByOther) { + }, [&](Finished) { + //updatePlaybackState(); + }); +} + +void Pip::handleStreamingError(Streaming::Error &&error) { + _panel.close(); +} + +void Pip::playbackPauseResume() { + if (_instance.player().finished() || !_instance.player().active()) { + //restartAtSeekPosition(0); + } else if (_instance.player().paused()) { + _instance.resume(); + //updatePlaybackState(); + //playbackPauseMusic(); + } else { + _instance.pause(); + //updatePlaybackState(); + } +} + +QImage Pip::videoFrame(const FrameRequest &request) const { + if (_instance.player().ready()) { + return _instance.frame(request); + } else if (_preparedCoverStorage.isNull() + || _preparedCoverRequest != request) { + _preparedCoverRequest = request; + _preparedCoverStorage = Streaming::PrepareByRequest( + _instance.info().video.cover, + _instance.info().video.rotation, + request, + std::move(_preparedCoverStorage)); + } + return _preparedCoverStorage; +} + +QImage Pip::videoFrameForDirectPaint(const FrameRequest &request) const { + const auto result = videoFrame(request); #ifdef USE_OPENGL_OVERLAY_WIDGET const auto bytesPerLine = result.bytesPerLine(); @@ -335,7 +469,7 @@ QImage Pip::videoFrameForDirectPaint() const { // // See Qt commit ed557c037847e343caa010562952b398f806adcd // - auto &cache = _streamed->frameForDirectPaint; + auto &cache = _frameForDirectPaint; if (cache.size() != result.size()) { cache = QImage(result.size(), result.format()); } diff --git a/Telegram/SourceFiles/media/view/media_view_pip.h b/Telegram/SourceFiles/media/view/media_view_pip.h index 90d0ee488..c36d27697 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.h +++ b/Telegram/SourceFiles/media/view/media_view_pip.h @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/rp_widget.h" +#include + namespace Media { namespace View { @@ -24,11 +26,23 @@ using PipParent = Ui::RpWidgetWrap; using PipParent = Ui::RpWidget; #endif // USE_OPENGL_OVERLAY_WIDGET -class Pip final : public PipParent { +class PipPanel final : public PipParent { public: - Pip( - std::shared_ptr document, - FnMut closeAndContinue); + struct Position { + RectParts attached = RectPart(0); + RectParts snapped = RectPart(0); + QRect geometry; + QRect screen; + }; + using FrameRequest = Streaming::FrameRequest; + + PipPanel( + QWidget *parent, + Fn paint); + + void setAspectRatio(QSize ratio); + [[nodiscard]] Position countPosition() const; + void setPosition(Position position); protected: void paintEvent(QPaintEvent *e) override; @@ -38,25 +52,21 @@ protected: void keyPressEvent(QKeyEvent *e) override; private: - void setupSize(); - void setupStreaming(); - void playbackPauseResume(); + void setPositionDefault(); + void setPositionOnScreen(Position position, QRect available); + void finishDrag(QPoint point); void updatePosition(QPoint point); - void waitingAnimationCallback(); - void handleStreamingUpdate(Streaming::Update &&update); - void handleStreamingError(Streaming::Error &&error); void updatePositionAnimated(); void moveAnimated(QPoint to); - [[nodiscard]] QImage videoFrame() const; - [[nodiscard]] QImage videoFrameForDirectPaint() const; + QPointer _parent; + Fn _paint; + RectParts _attached = RectParts(); + QSize _ratio; - Streaming::Instance _instance; - QSize _size; std::optional _pressPoint; std::optional _dragStartPosition; - FnMut _closeAndContinue; QPoint _positionAnimationFrom; QPoint _positionAnimationTo; @@ -64,5 +74,40 @@ private: }; +class Pip final { +public: + Pip( + QWidget *parent, + std::shared_ptr document, + FnMut closeAndContinue, + FnMut destroy); + +private: + using FrameRequest = Streaming::FrameRequest; + + void setupPanel(); + void setupStreaming(); + void paint(QPainter &p, const FrameRequest &request); + void playbackPauseResume(); + void waitingAnimationCallback(); + void handleStreamingUpdate(Streaming::Update &&update); + void handleStreamingError(Streaming::Error &&error); + + [[nodiscard]] QImage videoFrame(const FrameRequest &request) const; + [[nodiscard]] QImage videoFrameForDirectPaint( + const FrameRequest &request) const; + + Streaming::Instance _instance; + PipPanel _panel; + QSize _size; + FnMut _closeAndContinue; + FnMut _destroy; + + QImage _frameForDirectPaint; + mutable QImage _preparedCoverStorage; + mutable FrameRequest _preparedCoverRequest; + +}; + } // namespace View } // namespace Media diff --git a/Telegram/SourceFiles/media/view/mediaview.style b/Telegram/SourceFiles/media/view/mediaview.style index eaa574038..d604f0eb1 100644 --- a/Telegram/SourceFiles/media/view/mediaview.style +++ b/Telegram/SourceFiles/media/view/mediaview.style @@ -208,5 +208,7 @@ themePreviewCancelButton: RoundButton(defaultLightButton) { themePreviewButtonsSkip: 20px; themePreviewDialogsWidth: 312px; +pipDefaultSize: 320px; +pipMinimalSize: 100px; pipBorderSkip: 20px; pipBorderSnapArea: 16px;