1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "media/view/media_view_pip.h"
9
10 #include "media/streaming/media_streaming_player.h"
11 #include "media/streaming/media_streaming_document.h"
12 #include "media/streaming/media_streaming_utility.h"
13 #include "media/view/media_view_playback_progress.h"
14 #include "media/view/media_view_pip_opengl.h"
15 #include "media/view/media_view_pip_raster.h"
16 #include "media/audio/media_audio.h"
17 #include "data/data_document.h"
18 #include "data/data_document_media.h"
19 #include "data/data_file_origin.h"
20 #include "data/data_session.h"
21 #include "data/data_media_rotation.h"
22 #include "main/main_account.h"
23 #include "main/main_session.h"
24 #include "core/application.h"
25 #include "base/platform/base_platform_info.h"
26 #include "ui/platform/ui_platform_utility.h"
27 #include "ui/widgets/buttons.h"
28 #include "ui/wrap/fade_wrap.h"
29 #include "ui/widgets/shadow.h"
30 #include "ui/text/format_values.h"
31 #include "ui/gl/gl_surface.h"
32 #include "window/window_controller.h"
33 #include "styles/style_widgets.h"
34 #include "styles/style_window.h"
35 #include "styles/style_media_view.h"
36
37 #include <QtGui/QWindow>
38 #include <QtGui/QScreen>
39 #include <QtWidgets/QApplication>
40
41 namespace Media {
42 namespace View {
43 namespace {
44
45 constexpr auto kPipLoaderPriority = 2;
46 constexpr auto kMsInSecond = 1000;
47
IsWindowControlsOnLeft()48 [[nodiscard]] bool IsWindowControlsOnLeft() {
49 return Platform::IsMac();
50 }
51
ScreenFromPosition(QPoint point)52 [[nodiscard]] QRect ScreenFromPosition(QPoint point) {
53 const auto screen = QGuiApplication::screenAt(point);
54 const auto use = screen ? screen : QGuiApplication::primaryScreen();
55 return use
56 ? use->availableGeometry()
57 : QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight);
58 }
59
ClampToEdges(QRect screen,QRect inner)60 [[nodiscard]] QPoint ClampToEdges(QRect screen, QRect inner) {
61 const auto skip = st::pipBorderSkip;
62 const auto area = st::pipBorderSnapArea;
63 const auto sleft = screen.x() + skip;
64 const auto stop = screen.y() + skip;
65 const auto sright = screen.x() + screen.width() - skip;
66 const auto sbottom = screen.y() + screen.height() - skip;
67 const auto ileft = inner.x();
68 const auto itop = inner.y();
69 const auto iright = inner.x() + inner.width();
70 const auto ibottom = inner.y() + inner.height();
71 auto shiftx = 0;
72 auto shifty = 0;
73 if (iright + shiftx >= sright - area && iright + shiftx < sright + area) {
74 shiftx += (sright - iright);
75 }
76 if (ileft + shiftx >= sleft - area && ileft + shiftx < sleft + area) {
77 shiftx += (sleft - ileft);
78 }
79 if (ibottom + shifty >= sbottom - area && ibottom + shifty < sbottom + area) {
80 shifty += (sbottom - ibottom);
81 }
82 if (itop + shifty >= stop - area && itop + shifty < stop + area) {
83 shifty += (stop - itop);
84 }
85 return inner.topLeft() + QPoint(shiftx, shifty);
86 }
87
Transformed(QRect original,QSize minimalSize,QSize maximalSize,QPoint delta,RectPart by)88 [[nodiscard]] QRect Transformed(
89 QRect original,
90 QSize minimalSize,
91 QSize maximalSize,
92 QPoint delta,
93 RectPart by) {
94 const auto width = original.width();
95 const auto height = original.height();
96 const auto x1 = width - minimalSize.width();
97 const auto x2 = maximalSize.width() - width;
98 const auto y1 = height - minimalSize.height();
99 const auto y2 = maximalSize.height() - height;
100 switch (by) {
101 case RectPart::Center: return original.translated(delta);
102 case RectPart::TopLeft:
103 original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
104 original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
105 return original;
106 case RectPart::TopRight:
107 original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
108 original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
109 return original;
110 case RectPart::BottomRight:
111 original.setHeight(
112 original.height() + std::clamp(delta.y(), -y1, y2));
113 original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
114 return original;
115 case RectPart::BottomLeft:
116 original.setHeight(
117 original.height() + std::clamp(delta.y(), -y1, y2));
118 original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
119 return original;
120 case RectPart::Left:
121 original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
122 return original;
123 case RectPart::Top:
124 original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
125 return original;
126 case RectPart::Right:
127 original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
128 return original;
129 case RectPart::Bottom:
130 original.setHeight(
131 original.height() + std::clamp(delta.y(), -y1, y2));
132 return original;
133 }
134 return original;
135 Unexpected("RectPart in PiP Transformed.");
136 }
137
Constrained(QRect original,QSize minimalSize,QSize maximalSize,QSize ratio,RectPart by,RectParts attached)138 [[nodiscard]] QRect Constrained(
139 QRect original,
140 QSize minimalSize,
141 QSize maximalSize,
142 QSize ratio,
143 RectPart by,
144 RectParts attached) {
145 if (by == RectPart::Center) {
146 return original;
147 } else if (!original.width() && !original.height()) {
148 return QRect(original.topLeft(), ratio);
149 }
150 const auto widthLarger = (original.width() * ratio.height())
151 > (original.height() * ratio.width());
152 const auto desiredSize = ratio.scaled(
153 original.size(),
154 (((RectParts(by) & RectPart::AllCorners)
155 || ((by == RectPart::Top || by == RectPart::Bottom)
156 && widthLarger)
157 || ((by == RectPart::Left || by == RectPart::Right)
158 && !widthLarger))
159 ? Qt::KeepAspectRatio
160 : Qt::KeepAspectRatioByExpanding));
161 const auto newSize = QSize(
162 std::clamp(
163 desiredSize.width(),
164 minimalSize.width(),
165 maximalSize.width()),
166 std::clamp(
167 desiredSize.height(),
168 minimalSize.height(),
169 maximalSize.height()));
170 switch (by) {
171 case RectPart::TopLeft:
172 return QRect(
173 original.topLeft() + QPoint(
174 original.width() - newSize.width(),
175 original.height() - newSize.height()),
176 newSize);
177 case RectPart::TopRight:
178 return QRect(
179 original.topLeft() + QPoint(
180 0,
181 original.height() - newSize.height()),
182 newSize);
183 case RectPart::BottomRight:
184 return QRect(original.topLeft(), newSize);
185 case RectPart::BottomLeft:
186 return QRect(
187 original.topLeft() + QPoint(
188 original.width() - newSize.width(),
189 0),
190 newSize);
191 case RectPart::Left:
192 return QRect(
193 original.topLeft() + QPoint(
194 (original.width() - newSize.width()),
195 ((attached & RectPart::Top)
196 ? 0
197 : (attached & RectPart::Bottom)
198 ? (original.height() - newSize.height())
199 : (original.height() - newSize.height()) / 2)),
200 newSize);
201 case RectPart::Top:
202 return QRect(
203 original.topLeft() + QPoint(
204 ((attached & RectPart::Left)
205 ? 0
206 : (attached & RectPart::Right)
207 ? (original.width() - newSize.width())
208 : (original.width() - newSize.width()) / 2),
209 0),
210 newSize);
211 case RectPart::Right:
212 return QRect(
213 original.topLeft() + QPoint(
214 0,
215 ((attached & RectPart::Top)
216 ? 0
217 : (attached & RectPart::Bottom)
218 ? (original.height() - newSize.height())
219 : (original.height() - newSize.height()) / 2)),
220 newSize);
221 case RectPart::Bottom:
222 return QRect(
223 original.topLeft() + QPoint(
224 ((attached & RectPart::Left)
225 ? 0
226 : (attached & RectPart::Right)
227 ? (original.width() - newSize.width())
228 : (original.width() - newSize.width()) / 2),
229 (original.height() - newSize.height())),
230 newSize);
231 }
232 Unexpected("RectPart in PiP Constrained.");
233 }
234
Serialize(const PipPanel::Position & position)235 [[nodiscard]] QByteArray Serialize(const PipPanel::Position &position) {
236 auto result = QByteArray();
237 auto stream = QDataStream(&result, QIODevice::WriteOnly);
238 stream.setVersion(QDataStream::Qt_5_3);
239 stream
240 << qint32(position.attached.value())
241 << qint32(position.snapped.value())
242 << position.screen
243 << position.geometry;
244 stream.device()->close();
245
246 return result;
247 }
248
Deserialize(const QByteArray & data)249 [[nodiscard]] PipPanel::Position Deserialize(const QByteArray &data) {
250 auto stream = QDataStream(data);
251 auto result = PipPanel::Position();
252 auto attached = qint32(0);
253 auto snapped = qint32(0);
254 stream >> attached >> snapped >> result.screen >> result.geometry;
255 if (stream.status() != QDataStream::Ok) {
256 return {};
257 }
258 result.attached = RectParts::from_raw(attached);
259 result.snapped = RectParts::from_raw(snapped);
260 return result;
261 }
262
RectPartToQtEdges(RectPart rectPart)263 Qt::Edges RectPartToQtEdges(RectPart rectPart) {
264 switch (rectPart) {
265 case RectPart::TopLeft:
266 return Qt::TopEdge | Qt::LeftEdge;
267 case RectPart::TopRight:
268 return Qt::TopEdge | Qt::RightEdge;
269 case RectPart::BottomRight:
270 return Qt::BottomEdge | Qt::RightEdge;
271 case RectPart::BottomLeft:
272 return Qt::BottomEdge | Qt::LeftEdge;
273 case RectPart::Left:
274 return Qt::LeftEdge;
275 case RectPart::Top:
276 return Qt::TopEdge;
277 case RectPart::Right:
278 return Qt::RightEdge;
279 case RectPart::Bottom:
280 return Qt::BottomEdge;
281 }
282
283 return Qt::Edges();
284 }
285
286 } // namespace
287
RotatedRect(QRect rect,int rotation)288 QRect RotatedRect(QRect rect, int rotation) {
289 switch (rotation) {
290 case 0: return rect;
291 case 90: return QRect(
292 rect.y(),
293 -rect.x() - rect.width(),
294 rect.height(),
295 rect.width());
296 case 180: return QRect(
297 -rect.x() - rect.width(),
298 -rect.y() - rect.height(),
299 rect.width(),
300 rect.height());
301 case 270: return QRect(
302 -rect.y() - rect.height(),
303 rect.x(),
304 rect.height(),
305 rect.width());
306 }
307 Unexpected("Rotation in RotatedRect.");
308 }
309
UsePainterRotation(int rotation)310 bool UsePainterRotation(int rotation) {
311 return !(rotation % 180);
312 }
313
FlipSizeByRotation(QSize size,int rotation)314 QSize FlipSizeByRotation(QSize size, int rotation) {
315 return (((rotation / 90) % 2) == 1)
316 ? QSize(size.height(), size.width())
317 : size;
318 }
319
RotateFrameImage(QImage image,int rotation)320 QImage RotateFrameImage(QImage image, int rotation) {
321 auto transform = QTransform();
322 transform.rotate(rotation);
323 return image.transformed(transform);
324 }
325
PipPanel(QWidget * parent,Fn<Ui::GL::ChosenRenderer (Ui::GL::Capabilities)> renderer)326 PipPanel::PipPanel(
327 QWidget *parent,
328 Fn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer)
329 : _content(Ui::GL::CreateSurface(std::move(renderer)))
330 , _parent(parent) {
331 }
332
init()333 void PipPanel::init() {
334 widget()->setWindowFlags(Qt::Tool
335 | Qt::WindowStaysOnTopHint
336 | Qt::FramelessWindowHint
337 | Qt::WindowDoesNotAcceptFocus);
338 widget()->setAttribute(Qt::WA_ShowWithoutActivating);
339 widget()->setAttribute(Qt::WA_MacAlwaysShowToolWindow);
340 widget()->setAttribute(Qt::WA_NoSystemBackground);
341 widget()->setAttribute(Qt::WA_TranslucentBackground);
342 Ui::Platform::IgnoreAllActivation(widget());
343 Ui::Platform::InitOnTopPanel(widget());
344 widget()->setMouseTracking(true);
345 widget()->resize(0, 0);
346 widget()->hide();
347 widget()->createWinId();
348
349 rp()->shownValue(
350 ) | rpl::filter([=](bool shown) {
351 return shown;
352 }) | rpl::start_with_next([=] {
353 // Workaround Qt's forced transient parent.
354 Ui::Platform::ClearTransientParent(widget());
355 }, rp()->lifetime());
356 }
357
widget() const358 not_null<QWidget*> PipPanel::widget() const {
359 return _content->rpWidget();
360 }
361
rp() const362 not_null<Ui::RpWidgetWrap*> PipPanel::rp() const {
363 return _content.get();
364 }
365
setAspectRatio(QSize ratio)366 void PipPanel::setAspectRatio(QSize ratio) {
367 if (_ratio == ratio) {
368 return;
369 }
370 _ratio = ratio;
371 if (_ratio.isEmpty()) {
372 _ratio = QSize(1, 1);
373 }
374 if (!widget()->size().isEmpty()) {
375 setPosition(countPosition());
376 }
377 }
378
setPosition(Position position)379 void PipPanel::setPosition(Position position) {
380 if (!position.screen.isEmpty()) {
381 for (const auto screen : QApplication::screens()) {
382 if (screen->geometry() == position.screen) {
383 setPositionOnScreen(position, screen->availableGeometry());
384 return;
385 }
386 }
387 }
388 setPositionDefault();
389 }
390
inner() const391 QRect PipPanel::inner() const {
392 return widget()->rect().marginsRemoved(_padding);
393 }
394
attached() const395 RectParts PipPanel::attached() const {
396 return _attached;
397 }
398
useTransparency() const399 bool PipPanel::useTransparency() const {
400 return _useTransparency;
401 }
402
setDragDisabled(bool disabled)403 void PipPanel::setDragDisabled(bool disabled) {
404 _dragDisabled = disabled;
405 if (_dragState) {
406 _dragState = std::nullopt;
407 }
408 }
409
dragging() const410 bool PipPanel::dragging() const {
411 return _dragState.has_value();
412 }
413
saveGeometryRequests() const414 rpl::producer<> PipPanel::saveGeometryRequests() const {
415 return _saveGeometryRequests.events();
416 }
417
myScreen() const418 QScreen *PipPanel::myScreen() const {
419 if (const auto window = widget()->windowHandle()) {
420 return window->screen();
421 }
422 return nullptr;
423 }
424
countPosition() const425 PipPanel::Position PipPanel::countPosition() const {
426 const auto screen = myScreen();
427 if (!screen) {
428 return Position();
429 }
430 auto result = Position();
431 result.screen = screen->geometry();
432 result.geometry = widget()->geometry().marginsRemoved(_padding);
433 const auto available = screen->availableGeometry();
434 const auto skip = st::pipBorderSkip;
435 const auto left = result.geometry.x();
436 const auto right = left + result.geometry.width();
437 const auto top = result.geometry.y();
438 const auto bottom = top + result.geometry.height();
439 if ((!_dragState || *_dragState != RectPart::Center)
440 && !Platform::IsWayland()) {
441 if (left == available.x()) {
442 result.attached |= RectPart::Left;
443 } else if (right == available.x() + available.width()) {
444 result.attached |= RectPart::Right;
445 } else if (left == available.x() + skip) {
446 result.snapped |= RectPart::Left;
447 } else if (right == available.x() + available.width() - skip) {
448 result.snapped |= RectPart::Right;
449 }
450 if (top == available.y()) {
451 result.attached |= RectPart::Top;
452 } else if (bottom == available.y() + available.height()) {
453 result.attached |= RectPart::Bottom;
454 } else if (top == available.y() + skip) {
455 result.snapped |= RectPart::Top;
456 } else if (bottom == available.y() + available.height() - skip) {
457 result.snapped |= RectPart::Bottom;
458 }
459 }
460 return result;
461 }
462
setPositionDefault()463 void PipPanel::setPositionDefault() {
464 const auto widgetScreen = [&](auto &&widget) -> QScreen* {
465 if (auto handle = widget ? widget->windowHandle() : nullptr) {
466 return handle->screen();
467 }
468 return nullptr;
469 };
470 const auto parentScreen = widgetScreen(_parent);
471 const auto myScreen = widgetScreen(widget());
472 if (parentScreen && myScreen && myScreen != parentScreen) {
473 widget()->windowHandle()->setScreen(parentScreen);
474 }
475 const auto screen = parentScreen
476 ? parentScreen
477 : QGuiApplication::primaryScreen();
478 auto position = Position();
479 position.snapped = RectPart::Top | RectPart::Left;
480 position.screen = screen->geometry();
481 position.geometry = QRect(0, 0, st::pipDefaultSize, st::pipDefaultSize);
482 setPositionOnScreen(position, screen->availableGeometry());
483 }
484
setPositionOnScreen(Position position,QRect available)485 void PipPanel::setPositionOnScreen(Position position, QRect available) {
486 const auto screen = available;
487 const auto requestedSize = position.geometry.size();
488 const auto max = std::max(requestedSize.width(), requestedSize.height());
489
490 // Apply aspect ratio.
491 const auto scaled = (_ratio.width() > _ratio.height())
492 ? QSize(max, max * _ratio.height() / _ratio.width())
493 : QSize(max * _ratio.width() / _ratio.height(), max);
494
495 // At least one side should not be greater than half of screen size.
496 const auto byWidth = (scaled.width() * screen.height())
497 > (scaled.height() * screen.width());
498 const auto fit = QSize(screen.width() / 2, screen.height() / 2);
499 const auto normalized = (byWidth && scaled.width() > fit.width())
500 ? QSize(fit.width(), fit.width() * scaled.height() / scaled.width())
501 : (!byWidth && scaled.height() > fit.height())
502 ? QSize(
503 fit.height() * scaled.width() / scaled.height(),
504 fit.height())
505 : scaled;
506
507 // Apply minimal size.
508 const auto min = st::pipMinimalSize;
509 const auto minimalSize = (_ratio.width() > _ratio.height())
510 ? QSize(min * _ratio.width() / _ratio.height(), min)
511 : QSize(min, min * _ratio.height() / _ratio.width());
512 const auto size = QSize(
513 std::max(normalized.width(), minimalSize.width()),
514 std::max(normalized.height(), minimalSize.height()));
515
516 // Apply maximal size.
517 const auto maximalSize = (_ratio.width() > _ratio.height())
518 ? QSize(fit.width(), fit.width() * _ratio.height() / _ratio.width())
519 : QSize(fit.height() * _ratio.width() / _ratio.height(), fit.height());
520
521 // Apply left-right screen borders.
522 const auto skip = st::pipBorderSkip;
523 const auto inner = screen.marginsRemoved({ skip, skip, skip, skip });
524 auto geometry = QRect(position.geometry.topLeft(), size);
525 if ((position.attached & RectPart::Left)
526 || (geometry.x() < screen.x())) {
527 geometry.moveLeft(screen.x());
528 } else if ((position.attached & RectPart::Right)
529 || (geometry.x() + geometry.width() > screen.x() + screen.width())) {
530 geometry.moveLeft(screen.x() + screen.width() - geometry.width());
531 } else if (position.snapped & RectPart::Left) {
532 geometry.moveLeft(inner.x());
533 } else if (position.snapped & RectPart::Right) {
534 geometry.moveLeft(inner.x() + inner.width() - geometry.width());
535 }
536
537 // Apply top-bottom screen borders.
538 if ((position.attached & RectPart::Top) || (geometry.y() < screen.y())) {
539 geometry.moveTop(screen.y());
540 } else if ((position.attached & RectPart::Bottom)
541 || (geometry.y() + geometry.height()
542 > screen.y() + screen.height())) {
543 geometry.moveTop(
544 screen.y() + screen.height() - geometry.height());
545 } else if (position.snapped & RectPart::Top) {
546 geometry.moveTop(inner.y());
547 } else if (position.snapped & RectPart::Bottom) {
548 geometry.moveTop(inner.y() + inner.height() - geometry.height());
549 }
550
551 geometry += _padding;
552
553 setGeometry(geometry);
554 widget()->setMinimumSize(minimalSize);
555 widget()->setMaximumSize(
556 std::max(minimalSize.width(), maximalSize.width()),
557 std::max(minimalSize.height(), maximalSize.height()));
558 updateDecorations();
559 }
560
update()561 void PipPanel::update() {
562 widget()->update();
563 }
564
setGeometry(QRect geometry)565 void PipPanel::setGeometry(QRect geometry) {
566 widget()->setGeometry(geometry);
567 }
568
handleMousePress(QPoint position,Qt::MouseButton button)569 void PipPanel::handleMousePress(QPoint position, Qt::MouseButton button) {
570 if (button != Qt::LeftButton) {
571 return;
572 }
573 updateOverState(position);
574 _pressState = _overState;
575 _pressPoint = QCursor::pos();
576 }
577
handleMouseRelease(QPoint position,Qt::MouseButton button)578 void PipPanel::handleMouseRelease(QPoint position, Qt::MouseButton button) {
579 if (button != Qt::LeftButton || !base::take(_pressState)) {
580 return;
581 } else if (base::take(_dragState)) {
582 finishDrag(QCursor::pos());
583 updateOverState(position);
584 }
585 }
586
updateOverState(QPoint point)587 void PipPanel::updateOverState(QPoint point) {
588 const auto size = st::pipResizeArea;
589 const auto ignore = _attached | _snapped;
590 const auto count = [&](RectPart side, int padding) {
591 return (ignore & side) ? 0 : padding ? padding : size;
592 };
593 const auto left = count(RectPart::Left, _padding.left());
594 const auto top = count(RectPart::Top, _padding.top());
595 const auto right = count(RectPart::Right, _padding.right());
596 const auto bottom = count(RectPart::Bottom, _padding.bottom());
597 const auto width = widget()->width();
598 const auto height = widget()->height();
599 const auto overState = [&] {
600 if (point.x() < left) {
601 if (point.y() < top) {
602 return RectPart::TopLeft;
603 } else if (point.y() >= height - bottom) {
604 return RectPart::BottomLeft;
605 } else {
606 return RectPart::Left;
607 }
608 } else if (point.x() >= width - right) {
609 if (point.y() < top) {
610 return RectPart::TopRight;
611 } else if (point.y() >= height - bottom) {
612 return RectPart::BottomRight;
613 } else {
614 return RectPart::Right;
615 }
616 } else if (point.y() < top) {
617 return RectPart::Top;
618 } else if (point.y() >= height - bottom) {
619 return RectPart::Bottom;
620 } else {
621 return RectPart::Center;
622 }
623 }();
624 if (_overState != overState) {
625 _overState = overState;
626 widget()->setCursor([&] {
627 switch (_overState) {
628 case RectPart::Center:
629 return style::cur_pointer;
630 case RectPart::TopLeft:
631 case RectPart::BottomRight:
632 return style::cur_sizefdiag;
633 case RectPart::TopRight:
634 case RectPart::BottomLeft:
635 return style::cur_sizebdiag;
636 case RectPart::Left:
637 case RectPart::Right:
638 return style::cur_sizehor;
639 case RectPart::Top:
640 case RectPart::Bottom:
641 return style::cur_sizever;
642 }
643 Unexpected("State in PipPanel::updateOverState.");
644 }());
645 }
646 }
647
handleMouseMove(QPoint position)648 void PipPanel::handleMouseMove(QPoint position) {
649 if (!_pressState) {
650 updateOverState(position);
651 return;
652 }
653 const auto point = QCursor::pos();
654 const auto distance = QApplication::startDragDistance();
655 if (!_dragState
656 && (point - _pressPoint).manhattanLength() > distance
657 && !_dragDisabled) {
658 _dragState = _pressState;
659 updateDecorations();
660 _dragStartGeometry = widget()->geometry().marginsRemoved(_padding);
661 }
662 if (_dragState) {
663 if (Platform::IsWayland()) {
664 startSystemDrag();
665 } else {
666 processDrag(point);
667 }
668 }
669 }
670
startSystemDrag()671 void PipPanel::startSystemDrag() {
672 Expects(_dragState.has_value());
673
674 const auto stateEdges = RectPartToQtEdges(*_dragState);
675 if (stateEdges) {
676 widget()->windowHandle()->startSystemResize(stateEdges);
677 } else {
678 widget()->windowHandle()->startSystemMove();
679 }
680 }
681
processDrag(QPoint point)682 void PipPanel::processDrag(QPoint point) {
683 Expects(_dragState.has_value());
684
685 const auto dragPart = *_dragState;
686 const auto screen = (dragPart == RectPart::Center)
687 ? ScreenFromPosition(point)
688 : myScreen()
689 ? myScreen()->availableGeometry()
690 : QRect();
691 if (screen.isEmpty()) {
692 return;
693 }
694 const auto minimalSize = _ratio.scaled(
695 st::pipMinimalSize,
696 st::pipMinimalSize,
697 Qt::KeepAspectRatioByExpanding);
698 const auto maximalSize = _ratio.scaled(
699 screen.width() / 2,
700 screen.height() / 2,
701 Qt::KeepAspectRatio);
702 const auto geometry = Transformed(
703 _dragStartGeometry,
704 minimalSize,
705 maximalSize,
706 point - _pressPoint,
707 dragPart);
708 const auto valid = Constrained(
709 geometry,
710 minimalSize,
711 maximalSize,
712 _ratio,
713 dragPart,
714 _attached);
715 const auto clamped = (dragPart == RectPart::Center)
716 ? ClampToEdges(screen, valid)
717 : valid.topLeft();
718 if (clamped != valid.topLeft()) {
719 moveAnimated(clamped);
720 } else {
721 const auto newGeometry = valid.marginsAdded(_padding);
722 _positionAnimation.stop();
723 setGeometry(newGeometry);
724 }
725 }
726
finishDrag(QPoint point)727 void PipPanel::finishDrag(QPoint point) {
728 const auto screen = ScreenFromPosition(point);
729 const auto inner = widget()->geometry().marginsRemoved(_padding);
730 const auto position = widget()->pos();
731 const auto clamped = [&] {
732 auto result = position;
733 if (Platform::IsWayland()) {
734 return result;
735 }
736 if (result.x() > screen.x() + screen.width() - inner.width()) {
737 result.setX(screen.x() + screen.width() - inner.width());
738 }
739 if (result.x() < screen.x()) {
740 result.setX(screen.x());
741 }
742 if (result.y() > screen.y() + screen.height() - inner.height()) {
743 result.setY(screen.y() + screen.height() - inner.height());
744 }
745 if (result.y() < screen.y()) {
746 result.setY(screen.y());
747 }
748 return result;
749 }();
750 if (position != clamped) {
751 moveAnimated(clamped);
752 } else {
753 _positionAnimation.stop();
754 updateDecorations();
755 }
756 }
757
updatePositionAnimated()758 void PipPanel::updatePositionAnimated() {
759 const auto progress = _positionAnimation.value(1.);
760 if (!_positionAnimation.animating()) {
761 widget()->move(_positionAnimationTo
762 - QPoint(_padding.left(), _padding.top()));
763 if (!_dragState) {
764 updateDecorations();
765 }
766 return;
767 }
768 const auto from = QPointF(_positionAnimationFrom);
769 const auto to = QPointF(_positionAnimationTo);
770 widget()->move((from + (to - from) * progress).toPoint()
771 - QPoint(_padding.left(), _padding.top()));
772 }
773
moveAnimated(QPoint to)774 void PipPanel::moveAnimated(QPoint to) {
775 if (_positionAnimation.animating() && _positionAnimationTo == to) {
776 return;
777 }
778 _positionAnimationTo = to;
779 _positionAnimationFrom = widget()->pos()
780 + QPoint(_padding.left(), _padding.top());
781 _positionAnimation.stop();
782 _positionAnimation.start(
783 [=] { updatePositionAnimated(); },
784 0.,
785 1.,
786 st::slideWrapDuration,
787 anim::easeOutCirc);
788 }
789
updateDecorations()790 void PipPanel::updateDecorations() {
791 const auto guard = gsl::finally([&] {
792 if (!_dragState) {
793 _saveGeometryRequests.fire({});
794 }
795 });
796 const auto position = countPosition();
797 const auto center = position.geometry.center();
798 const auto use = Ui::Platform::TranslucentWindowsSupported(center);
799 const auto full = use ? st::callShadow.extend : style::margins();
800 const auto padding = style::margins(
801 (position.attached & RectPart::Left) ? 0 : full.left(),
802 (position.attached & RectPart::Top) ? 0 : full.top(),
803 (position.attached & RectPart::Right) ? 0 : full.right(),
804 (position.attached & RectPart::Bottom) ? 0 : full.bottom());
805 _snapped = position.snapped;
806 if (_padding == padding && _attached == position.attached) {
807 return;
808 }
809 const auto newGeometry = position.geometry.marginsAdded(padding);
810 _attached = position.attached;
811 _padding = padding;
812 _useTransparency = use;
813 widget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
814 setGeometry(newGeometry);
815 update();
816 }
817
Pip(not_null<Delegate * > delegate,not_null<DocumentData * > data,FullMsgId contextId,std::shared_ptr<Streaming::Document> shared,FnMut<void ()> closeAndContinue,FnMut<void ()> destroy)818 Pip::Pip(
819 not_null<Delegate*> delegate,
820 not_null<DocumentData*> data,
821 FullMsgId contextId,
822 std::shared_ptr<Streaming::Document> shared,
823 FnMut<void()> closeAndContinue,
824 FnMut<void()> destroy)
825 : _delegate(delegate)
826 , _data(data)
827 , _contextId(contextId)
828 , _instance(std::move(shared), [=] { waitingAnimationCallback(); })
829 , _panel(
830 _delegate->pipParentWidget(),
__anone86b8e890c02(Ui::GL::Capabilities capabilities) 831 [=](Ui::GL::Capabilities capabilities) {
832 return chooseRenderer(capabilities);
833 })
834 , _playbackProgress(std::make_unique<PlaybackProgress>())
835 , _rotation(data->owner().mediaRotation().get(data))
836 , _lastPositiveVolume((Core::App().settings().videoVolume() > 0.)
837 ? Core::App().settings().videoVolume()
838 : Core::Settings::kDefaultVolume)
839 , _closeAndContinue(std::move(closeAndContinue))
840 , _destroy(std::move(destroy)) {
841 setupPanel();
842 setupButtons();
843 setupStreaming();
844
845 _data->session().account().sessionChanges(
__anone86b8e890d02null846 ) | rpl::start_with_next([=] {
847 _destroy();
848 }, _panel.rp()->lifetime());
849 }
850
851 Pip::~Pip() = default;
852
setupPanel()853 void Pip::setupPanel() {
854 _panel.init();
855 const auto size = [&] {
856 if (!_instance.info().video.size.isEmpty()) {
857 return _instance.info().video.size;
858 }
859 const auto media = _data->activeMediaView();
860 if (media) {
861 media->goodThumbnailWanted();
862 }
863 const auto good = media ? media->goodThumbnail() : nullptr;
864 const auto original = good ? good->size() : _data->dimensions;
865 return original.isEmpty() ? QSize(1, 1) : original;
866 }();
867 _panel.setAspectRatio(FlipSizeByRotation(size, _rotation));
868 _panel.setPosition(Deserialize(_delegate->pipLoadGeometry()));
869 _panel.widget()->show();
870
871 _panel.saveGeometryRequests(
872 ) | rpl::start_with_next([=] {
873 saveGeometry();
874 }, _panel.rp()->lifetime());
875
876 _panel.rp()->events(
877 ) | rpl::start_with_next([=](not_null<QEvent*> e) {
878 const auto mousePosition = [&] {
879 return static_cast<QMouseEvent*>(e.get())->pos();
880 };
881 const auto mouseButton = [&] {
882 return static_cast<QMouseEvent*>(e.get())->button();
883 };
884 switch (e->type()) {
885 case QEvent::Close: handleClose(); break;
886 case QEvent::Leave: handleLeave(); break;
887 case QEvent::MouseMove:
888 handleMouseMove(mousePosition());
889 break;
890 case QEvent::MouseButtonPress:
891 handleMousePress(mousePosition(), mouseButton());
892 break;
893 case QEvent::MouseButtonRelease:
894 handleMouseRelease(mousePosition(), mouseButton());
895 break;
896 case QEvent::MouseButtonDblClick:
897 handleDoubleClick(mouseButton());
898 break;
899 }
900 }, _panel.rp()->lifetime());
901 }
902
handleClose()903 void Pip::handleClose() {
904 crl::on_main(_panel.widget(), [=] {
905 _destroy();
906 });
907 }
908
handleLeave()909 void Pip::handleLeave() {
910 setOverState(OverState::None);
911 }
912
handleMouseMove(QPoint position)913 void Pip::handleMouseMove(QPoint position) {
914 const auto weak = Ui::MakeWeak(_panel.widget());
915 const auto guard = gsl::finally([&] {
916 if (weak) {
917 _panel.handleMouseMove(position);
918 }
919 });
920 setOverState(computeState(position));
921 seekUpdate(position);
922 volumeControllerUpdate(position);
923 }
924
setOverState(OverState state)925 void Pip::setOverState(OverState state) {
926 if (_over == state) {
927 return;
928 }
929 const auto wasShown = ResolveShownOver(_over);
930 _over = state;
931 const auto nowAreShown = (ResolveShownOver(_over) != OverState::None);
932 if ((wasShown != OverState::None) != nowAreShown) {
933 _controlsShown.start(
934 [=] { _panel.update(); },
935 nowAreShown ? 0. : 1.,
936 nowAreShown ? 1. : 0.,
937 st::fadeWrapDuration,
938 anim::linear);
939 }
940 if (!_pressed) {
941 updateActiveState(wasShown);
942 }
943 _panel.update();
944 }
945
setPressedState(std::optional<OverState> state)946 void Pip::setPressedState(std::optional<OverState> state) {
947 if (_pressed == state) {
948 return;
949 }
950 const auto wasShown = shownActiveState();
951 _pressed = state;
952 updateActiveState(wasShown);
953 }
954
shownActiveState() const955 Pip::OverState Pip::shownActiveState() const {
956 return ResolveShownOver(_pressed.value_or(_over));
957 }
958
activeValue(const Button & button) const959 float64 Pip::activeValue(const Button &button) const {
960 const auto shownState = ResolveShownOver(button.state);
961 return button.active.value((shownActiveState() == shownState) ? 1. : 0.);
962 }
963
updateActiveState(OverState wasShown)964 void Pip::updateActiveState(OverState wasShown) {
965 const auto check = [&](Button &button) {
966 const auto shownState = ResolveShownOver(button.state);
967 const auto nowIsShown = (shownActiveState() == shownState);
968 if ((wasShown == shownState) != nowIsShown) {
969 button.active.start(
970 [=, &button] { _panel.widget()->update(button.icon); },
971 nowIsShown ? 0. : 1.,
972 nowIsShown ? 1. : 0.,
973 st::fadeWrapDuration,
974 anim::linear);
975 }
976 };
977 check(_close);
978 check(_enlarge);
979 check(_play);
980 check(_playback);
981 check(_volumeToggle);
982 check(_volumeController);
983 }
984
ResolveShownOver(OverState state)985 Pip::OverState Pip::ResolveShownOver(OverState state) {
986 return (state == OverState::VolumeController)
987 ? OverState::VolumeToggle
988 : state;
989 }
990
handleMousePress(QPoint position,Qt::MouseButton button)991 void Pip::handleMousePress(QPoint position, Qt::MouseButton button) {
992 const auto weak = Ui::MakeWeak(_panel.widget());
993 const auto guard = gsl::finally([&] {
994 if (weak) {
995 _panel.handleMousePress(position, button);
996 }
997 });
998 if (button != Qt::LeftButton) {
999 return;
1000 }
1001 _pressed = _over;
1002 if (_over == OverState::Playback || _over == OverState::VolumeController) {
1003 _panel.setDragDisabled(true);
1004 }
1005 seekUpdate(position);
1006 volumeControllerUpdate(position);
1007 }
1008
handleMouseRelease(QPoint position,Qt::MouseButton button)1009 void Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) {
1010 Expects(1 && _delegate->pipPlaybackSpeed() >= 0.5
1011 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1012
1013 const auto weak = Ui::MakeWeak(_panel.widget());
1014 const auto guard = gsl::finally([&] {
1015 if (weak) {
1016 _panel.handleMouseRelease(position, button);
1017 }
1018 });
1019 if (button != Qt::LeftButton) {
1020 return;
1021 }
1022 seekUpdate(position);
1023
1024 Assert(2 && _delegate->pipPlaybackSpeed() >= 0.5
1025 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1026
1027 volumeControllerUpdate(position);
1028
1029 Assert(3 && _delegate->pipPlaybackSpeed() >= 0.5
1030 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1031
1032 const auto pressed = base::take(_pressed);
1033 if (pressed && *pressed == OverState::Playback) {
1034 _panel.setDragDisabled(false);
1035
1036 Assert(4 && _delegate->pipPlaybackSpeed() >= 0.5
1037 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1038
1039 seekFinish(_playbackProgress->value());
1040 } else if (pressed && *pressed == OverState::VolumeController) {
1041 _panel.setDragDisabled(false);
1042 _panel.update();
1043 } else if (_panel.dragging() || !pressed || *pressed != _over) {
1044 _lastHandledPress = std::nullopt;
1045 } else {
1046 _lastHandledPress = _over;
1047 switch (_over) {
1048 case OverState::Close: _panel.widget()->close(); break;
1049 case OverState::Enlarge: _closeAndContinue(); break;
1050 case OverState::VolumeToggle: volumeToggled(); break;
1051 case OverState::Other: playbackPauseResume(); break;
1052 }
1053 }
1054 }
1055
handleDoubleClick(Qt::MouseButton button)1056 void Pip::handleDoubleClick(Qt::MouseButton button) {
1057 if (_over != OverState::Other
1058 || !_lastHandledPress
1059 || *_lastHandledPress != _over) {
1060 return;
1061 }
1062 playbackPauseResume(); // Un-click the first click.
1063 _closeAndContinue();
1064 }
1065
seekUpdate(QPoint position)1066 void Pip::seekUpdate(QPoint position) {
1067 if (!_pressed || *_pressed != OverState::Playback) {
1068 return;
1069 }
1070 const auto unbound = (position.x() - _playback.icon.x())
1071 / float64(_playback.icon.width());
1072 const auto progress = std::clamp(unbound, 0., 1.);
1073 seekProgress(progress);
1074 }
1075
seekProgress(float64 value)1076 void Pip::seekProgress(float64 value) {
1077 if (!_lastDurationMs) {
1078 return;
1079 }
1080
1081 _playbackProgress->setValue(value, false);
1082
1083 const auto positionMs = std::clamp(
1084 static_cast<crl::time>(value * _lastDurationMs),
1085 crl::time(0),
1086 _lastDurationMs);
1087 if (_seekPositionMs != positionMs) {
1088 _seekPositionMs = positionMs;
1089 if (!_instance.player().paused()
1090 && !_instance.player().finished()) {
1091 _pausedBySeek = true;
1092 playbackPauseResume();
1093 }
1094 updatePlaybackTexts(_seekPositionMs, _lastDurationMs, kMsInSecond);
1095 }
1096 }
1097
seekFinish(float64 value)1098 void Pip::seekFinish(float64 value) {
1099 Expects(5 && _delegate->pipPlaybackSpeed() >= 0.5
1100 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1101
1102 if (!_lastDurationMs) {
1103 return;
1104 }
1105
1106 const auto positionMs = std::clamp(
1107 static_cast<crl::time>(value * _lastDurationMs),
1108 crl::time(0),
1109 _lastDurationMs);
1110 _seekPositionMs = -1;
1111 _startPaused = !_pausedBySeek && !_instance.player().finished();
1112 restartAtSeekPosition(positionMs);
1113 }
1114
volumeChanged(float64 volume)1115 void Pip::volumeChanged(float64 volume) {
1116 if (volume > 0.) {
1117 _lastPositiveVolume = volume;
1118 }
1119 Player::mixer()->setVideoVolume(volume);
1120 Core::App().settings().setVideoVolume(volume);
1121 Core::App().saveSettingsDelayed();
1122 }
1123
volumeToggled()1124 void Pip::volumeToggled() {
1125 const auto volume = Core::App().settings().videoVolume();
1126 volumeChanged(volume ? 0. : _lastPositiveVolume);
1127 _panel.update();
1128 }
1129
volumeControllerUpdate(QPoint position)1130 void Pip::volumeControllerUpdate(QPoint position) {
1131 if (!_pressed || *_pressed != OverState::VolumeController) {
1132 return;
1133 }
1134 const auto unbound = (position.x() - _volumeController.icon.x())
1135 / float64(_volumeController.icon.width());
1136 const auto value = std::clamp(unbound, 0., 1.);
1137 volumeChanged(value);
1138 _panel.update();
1139 }
1140
setupButtons()1141 void Pip::setupButtons() {
1142 _close.state = OverState::Close;
1143 _enlarge.state = OverState::Enlarge;
1144 _playback.state = OverState::Playback;
1145 _volumeToggle.state = OverState::VolumeToggle;
1146 _volumeController.state = OverState::VolumeController;
1147 _play.state = OverState::Other;
1148 _panel.rp()->sizeValue(
1149 ) | rpl::map([=] {
1150 return _panel.inner();
1151 }) | rpl::start_with_next([=](QRect rect) {
1152 const auto skip = st::pipControlSkip;
1153 _close.area = QRect(
1154 rect.x(),
1155 rect.y(),
1156 st::pipCloseIcon.width() + 2 * skip,
1157 st::pipCloseIcon.height() + 2 * skip);
1158 _enlarge.area = QRect(
1159 _close.area.x() + _close.area.width(),
1160 rect.y(),
1161 st::pipEnlargeIcon.width() + 2 * skip,
1162 st::pipEnlargeIcon.height() + 2 * skip);
1163
1164 const auto volumeSkip = st::pipPlaybackSkip;
1165 const auto volumeHeight = 2 * volumeSkip + st::pipPlaybackWide;
1166 const auto volumeToggleWidth = st::pipVolumeIcon0.width()
1167 + 2 * skip;
1168 const auto volumeToggleHeight = st::pipVolumeIcon0.height()
1169 + 2 * skip;
1170 const auto volumeWidth = (((st::mediaviewVolumeWidth + 2 * skip)
1171 + _close.area.width()
1172 + _enlarge.area.width()
1173 + volumeToggleWidth) < rect.width())
1174 ? st::mediaviewVolumeWidth
1175 : 0;
1176 _volumeController.area = QRect(
1177 rect.x() + rect.width() - volumeWidth - 2 * volumeSkip,
1178 rect.y() + (volumeToggleHeight - volumeHeight) / 2,
1179 volumeWidth,
1180 volumeHeight);
1181 _volumeToggle.area = QRect(
1182 _volumeController.area.x()
1183 - st::pipVolumeIcon0.width()
1184 - skip,
1185 rect.y(),
1186 volumeToggleWidth,
1187 volumeToggleHeight);
1188 if (!IsWindowControlsOnLeft()) {
1189 _close.area.moveLeft(rect.x()
1190 + rect.width()
1191 - (_close.area.x() - rect.x())
1192 - _close.area.width());
1193 _enlarge.area.moveLeft(rect.x()
1194 + rect.width()
1195 - (_enlarge.area.x() - rect.x())
1196 - _enlarge.area.width());
1197 _volumeToggle.area.moveLeft(rect.x());
1198 _volumeController.area.moveLeft(_volumeToggle.area.x()
1199 + _volumeToggle.area.width());
1200 }
1201 _close.icon = _close.area.marginsRemoved({ skip, skip, skip, skip });
1202 _enlarge.icon = _enlarge.area.marginsRemoved(
1203 { skip, skip, skip, skip });
1204 _volumeToggle.icon = _volumeToggle.area.marginsRemoved(
1205 { skip, skip, skip, skip });
1206 _play.icon = QRect(
1207 rect.x() + (rect.width() - st::pipPlayIcon.width()) / 2,
1208 rect.y() + (rect.height() - st::pipPlayIcon.height()) / 2,
1209 st::pipPlayIcon.width(),
1210 st::pipPlayIcon.height());
1211 const auto volumeArea = _volumeController.area;
1212 _volumeController.icon = (volumeArea.width() > 2 * volumeSkip
1213 && volumeArea.height() > 2 * volumeSkip)
1214 ? volumeArea.marginsRemoved(
1215 { volumeSkip, volumeSkip, volumeSkip, volumeSkip })
1216 : QRect();
1217 const auto playbackSkip = st::pipPlaybackSkip;
1218 const auto playbackHeight = 2 * playbackSkip + st::pipPlaybackWide;
1219 _playback.area = QRect(
1220 rect.x(),
1221 rect.y() + rect.height() - playbackHeight,
1222 rect.width(),
1223 playbackHeight);
1224 _playback.icon = _playback.area.marginsRemoved(
1225 { playbackSkip, playbackSkip, playbackSkip, playbackSkip });
1226 }, _panel.rp()->lifetime());
1227
1228 _playbackProgress->setValueChangedCallback([=](
1229 float64 value,
1230 float64 receivedTill) {
1231 _panel.widget()->update(_playback.area);
1232 });
1233 }
1234
saveGeometry()1235 void Pip::saveGeometry() {
1236 _delegate->pipSaveGeometry(Serialize(_panel.countPosition()));
1237 }
1238
updatePlayPauseResumeState(const Player::TrackState & state)1239 void Pip::updatePlayPauseResumeState(const Player::TrackState &state) {
1240 auto showPause = Player::ShowPauseIcon(state.state);
1241 if (showPause != _showPause) {
1242 _showPause = showPause;
1243 _panel.update();
1244 }
1245 }
1246
setupStreaming()1247 void Pip::setupStreaming() {
1248 _instance.setPriority(kPipLoaderPriority);
1249 _instance.lockPlayer();
1250
1251 _instance.player().updates(
1252 ) | rpl::start_with_next_error([=](Streaming::Update &&update) {
1253 handleStreamingUpdate(std::move(update));
1254 }, [=](Streaming::Error &&error) {
1255 handleStreamingError(std::move(error));
1256 }, _instance.lifetime());
1257 updatePlaybackState();
1258 }
1259
chooseRenderer(Ui::GL::Capabilities capabilities)1260 Ui::GL::ChosenRenderer Pip::chooseRenderer(
1261 Ui::GL::Capabilities capabilities) {
1262 const auto use = Platform::IsMac()
1263 ? true
1264 : capabilities.transparency;
1265 LOG(("OpenGL: %1 (PipPanel)").arg(Logs::b(use)));
1266 if (use) {
1267 _opengl = true;
1268 return {
1269 .renderer = std::make_unique<RendererGL>(this),
1270 .backend = Ui::GL::Backend::OpenGL,
1271 };
1272 }
1273 return {
1274 .renderer = std::make_unique<RendererSW>(this),
1275 .backend = Ui::GL::Backend::Raster,
1276 };
1277 }
1278
paint(not_null<Renderer * > renderer) const1279 void Pip::paint(not_null<Renderer*> renderer) const {
1280 const auto controlsShown = _controlsShown.value(
1281 (_over != OverState::None) ? 1. : 0.);
1282 auto geometry = ContentGeometry{
1283 .inner = _panel.inner(),
1284 .attached = (_panel.useTransparency()
1285 ? _panel.attached()
1286 : RectPart::AllSides),
1287 .fade = controlsShown,
1288 .outer = _panel.widget()->size(),
1289 .rotation = _rotation,
1290 .videoRotation = _instance.info().video.rotation,
1291 .useTransparency = _panel.useTransparency(),
1292 };
1293 if (canUseVideoFrame()) {
1294 renderer->paintTransformedVideoFrame(geometry);
1295 _instance.markFrameShown();
1296 } else {
1297 const auto content = staticContent();
1298 if (_preparedCoverState == ThumbState::Cover) {
1299 geometry.rotation += base::take(geometry.videoRotation);
1300 }
1301 renderer->paintTransformedStaticContent(staticContent(), geometry);
1302 }
1303 if (_instance.waitingShown()) {
1304 renderer->paintRadialLoading(countRadialRect(), controlsShown);
1305 }
1306 if (controlsShown > 0) {
1307 paintButtons(renderer, controlsShown);
1308 paintPlayback(renderer, controlsShown);
1309 paintVolumeController(renderer, controlsShown);
1310 }
1311 }
1312
paintButtons(not_null<Renderer * > renderer,float64 shown) const1313 void Pip::paintButtons(not_null<Renderer*> renderer, float64 shown) const {
1314 const auto outer = _panel.widget()->width();
1315 const auto drawOne = [&](
1316 const Button &button,
1317 const style::icon &icon,
1318 const style::icon &iconOver) {
1319 renderer->paintButton(
1320 button,
1321 outer,
1322 shown,
1323 activeValue(button),
1324 icon,
1325 iconOver);
1326 };
1327
1328 renderer->paintButtonsStart();
1329 drawOne(
1330 _play,
1331 _showPause ? st::pipPauseIcon : st::pipPlayIcon,
1332 _showPause ? st::pipPauseIconOver : st::pipPlayIconOver);
1333 drawOne(_close, st::pipCloseIcon, st::pipCloseIconOver);
1334 drawOne(_enlarge, st::pipEnlargeIcon, st::pipEnlargeIconOver);
1335 const auto volume = Core::App().settings().videoVolume();
1336 if (volume <= 0.) {
1337 drawOne(
1338 _volumeToggle,
1339 st::pipVolumeIcon0,
1340 st::pipVolumeIcon0Over);
1341 } else if (volume < 1 / 2.) {
1342 drawOne(
1343 _volumeToggle,
1344 st::pipVolumeIcon1,
1345 st::pipVolumeIcon1Over);
1346 } else {
1347 drawOne(
1348 _volumeToggle,
1349 st::pipVolumeIcon2,
1350 st::pipVolumeIcon2Over);
1351 }
1352 }
1353
paintPlayback(not_null<Renderer * > renderer,float64 shown) const1354 void Pip::paintPlayback(not_null<Renderer*> renderer, float64 shown) const {
1355 const auto outer = QRect(
1356 _playback.icon.x(),
1357 _playback.icon.y() - st::pipPlaybackFont->height,
1358 _playback.icon.width(),
1359 st::pipPlaybackFont->height + _playback.icon.height());
1360 renderer->paintPlayback(outer, shown);
1361 }
1362
paintPlaybackContent(QPainter & p,QRect outer,float64 shown) const1363 void Pip::paintPlaybackContent(
1364 QPainter &p,
1365 QRect outer,
1366 float64 shown) const {
1367 p.setOpacity(shown);
1368 paintPlaybackProgress(p, outer);
1369 paintPlaybackTexts(p, outer);
1370 }
1371
paintPlaybackProgress(QPainter & p,QRect outer) const1372 void Pip::paintPlaybackProgress(QPainter &p, QRect outer) const {
1373 const auto radius = _playback.icon.height() / 2;
1374 const auto progress = _playbackProgress->value();
1375 const auto active = activeValue(_playback);
1376 const auto height = anim::interpolate(
1377 st::pipPlaybackWidth,
1378 _playback.icon.height(),
1379 active);
1380 const auto rect = QRect(
1381 outer.x(),
1382 (outer.y()
1383 + st::pipPlaybackFont->height
1384 + _playback.icon.height()
1385 - height),
1386 outer.width(),
1387 height);
1388
1389 paintProgressBar(p, rect, progress, radius, active);
1390 }
1391
paintProgressBar(QPainter & p,const QRect & rect,float64 progress,int radius,float64 active) const1392 void Pip::paintProgressBar(
1393 QPainter &p,
1394 const QRect &rect,
1395 float64 progress,
1396 int radius,
1397 float64 active) const {
1398 const auto done = int(base::SafeRound(rect.width() * progress));
1399 PainterHighQualityEnabler hq(p);
1400 p.setPen(Qt::NoPen);
1401 if (done > 0) {
1402 p.setBrush(anim::brush(
1403 st::mediaviewPipControlsFg,
1404 st::mediaviewPipPlaybackActive,
1405 active));
1406 p.setClipRect(rect.x(), rect.y(), done, rect.height());
1407 p.drawRoundedRect(
1408 rect.x(),
1409 rect.y(),
1410 std::min(done + radius, rect.width()),
1411 rect.height(),
1412 radius,
1413 radius);
1414 }
1415 if (done < rect.width()) {
1416 const auto from = std::max(rect.x() + done - radius, rect.x());
1417 p.setBrush(st::mediaviewPipPlaybackInactive);
1418 p.setClipRect(
1419 rect.x() + done,
1420 rect.y(),
1421 rect.width() - done,
1422 rect.height());
1423 p.drawRoundedRect(
1424 from,
1425 rect.y(),
1426 rect.x() + rect.width() - from,
1427 rect.height(),
1428 radius,
1429 radius);
1430 }
1431 p.setClipping(false);
1432 }
1433
paintPlaybackTexts(QPainter & p,QRect outer) const1434 void Pip::paintPlaybackTexts(QPainter &p, QRect outer) const {
1435 const auto left = outer.x()
1436 - _playback.icon.x()
1437 + _playback.area.x()
1438 + st::pipPlaybackTextSkip;
1439 const auto right = outer.x()
1440 - _playback.icon.x()
1441 + _playback.area.x()
1442 + _playback.area.width()
1443 - st::pipPlaybackTextSkip;
1444 const auto top = outer.y() + st::pipPlaybackFont->ascent;
1445
1446 p.setFont(st::pipPlaybackFont);
1447 p.setPen(st::mediaviewPipControlsFgOver);
1448 p.drawText(left, top, _timeAlready);
1449 p.drawText(right - _timeLeftWidth, top, _timeLeft);
1450 }
1451
paintVolumeController(not_null<Renderer * > renderer,float64 shown) const1452 void Pip::paintVolumeController(
1453 not_null<Renderer*> renderer,
1454 float64 shown) const {
1455 if (_volumeController.icon.isEmpty()) {
1456 return;
1457 }
1458 renderer->paintVolumeController(_volumeController.icon, shown);
1459 }
1460
paintVolumeControllerContent(QPainter & p,QRect outer,float64 shown) const1461 void Pip::paintVolumeControllerContent(
1462 QPainter &p,
1463 QRect outer,
1464 float64 shown) const {
1465 p.setOpacity(shown);
1466
1467 const auto radius = _volumeController.icon.height() / 2;
1468 const auto volume = Core::App().settings().videoVolume();
1469 const auto active = activeValue(_volumeController);
1470 const auto height = anim::interpolate(
1471 st::pipPlaybackWidth,
1472 _volumeController.icon.height(),
1473 active);
1474 const auto rect = QRect(
1475 outer.x(),
1476 outer.y() + radius - height / 2,
1477 outer.width(),
1478 height);
1479
1480 paintProgressBar(p, rect, volume, radius, active);
1481 }
1482
handleStreamingUpdate(Streaming::Update && update)1483 void Pip::handleStreamingUpdate(Streaming::Update &&update) {
1484 using namespace Streaming;
1485
1486 v::match(update.data, [&](Information &update) {
1487 _panel.setAspectRatio(
1488 FlipSizeByRotation(update.video.size, _rotation));
1489 }, [&](const PreloadedVideo &update) {
1490 updatePlaybackState();
1491 }, [&](const UpdateVideo &update) {
1492 _panel.update();
1493 Core::App().updateNonIdle();
1494 updatePlaybackState();
1495 }, [&](const PreloadedAudio &update) {
1496 updatePlaybackState();
1497 }, [&](const UpdateAudio &update) {
1498 updatePlaybackState();
1499 }, [&](WaitingForData) {
1500 }, [&](MutedByOther) {
1501 }, [&](Finished) {
1502 updatePlaybackState();
1503 });
1504 }
1505
updatePlaybackState()1506 void Pip::updatePlaybackState() {
1507 const auto state = _instance.player().prepareLegacyState();
1508 updatePlayPauseResumeState(state);
1509 if (state.position == kTimeUnknown
1510 || state.length == kTimeUnknown
1511 || _pausedBySeek) {
1512 return;
1513 }
1514 _playbackProgress->updateState(state);
1515
1516 qint64 position = 0;
1517 if (Player::IsStoppedAtEnd(state.state)) {
1518 position = state.length;
1519 } else if (!Player::IsStoppedOrStopping(state.state)) {
1520 position = state.position;
1521 } else {
1522 position = 0;
1523 }
1524 const auto playFrequency = state.frequency;
1525 _lastDurationMs = (state.length * crl::time(1000)) / playFrequency;
1526
1527 if (_seekPositionMs < 0) {
1528 updatePlaybackTexts(position, state.length, playFrequency);
1529 }
1530 }
1531
updatePlaybackTexts(int64 position,int64 length,int64 frequency)1532 void Pip::updatePlaybackTexts(
1533 int64 position,
1534 int64 length,
1535 int64 frequency) {
1536 const auto playAlready = position / frequency;
1537 const auto playLeft = (length / frequency) - playAlready;
1538 const auto already = Ui::FormatDurationText(playAlready);
1539 const auto minus = QChar(8722);
1540 const auto left = minus + Ui::FormatDurationText(playLeft);
1541 if (_timeAlready == already && _timeLeft == left) {
1542 return;
1543 }
1544 _timeAlready = already;
1545 _timeLeft = left;
1546 _timeLeftWidth = st::pipPlaybackFont->width(_timeLeft);
1547 _panel.widget()->update(QRect(
1548 _playback.area.x(),
1549 _playback.icon.y() - st::pipPlaybackFont->height,
1550 _playback.area.width(),
1551 st::pipPlaybackFont->height));
1552 }
1553
handleStreamingError(Streaming::Error && error)1554 void Pip::handleStreamingError(Streaming::Error &&error) {
1555 _panel.widget()->close();
1556 }
1557
playbackPauseResume()1558 void Pip::playbackPauseResume() {
1559 if (_instance.player().failed()) {
1560 _panel.widget()->close();
1561 } else if (_instance.player().finished()
1562 || !_instance.player().active()) {
1563 _startPaused = false;
1564 restartAtSeekPosition(0);
1565 } else if (_instance.player().paused()) {
1566 _instance.resume();
1567 updatePlaybackState();
1568 } else {
1569 _instance.pause();
1570 updatePlaybackState();
1571 }
1572 }
1573
restartAtSeekPosition(crl::time position)1574 void Pip::restartAtSeekPosition(crl::time position) {
1575 Expects(6 && _delegate->pipPlaybackSpeed() >= 0.5
1576 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1577
1578 if (!_instance.info().video.cover.isNull()) {
1579 _preparedCoverStorage = QImage();
1580 _preparedCoverState = ThumbState::Empty;
1581 _instance.saveFrameToCover();
1582 }
1583
1584 Assert(7 && _delegate->pipPlaybackSpeed() >= 0.5
1585 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1586
1587 auto options = Streaming::PlaybackOptions();
1588 options.position = position;
1589 options.audioId = _instance.player().prepareLegacyState().id;
1590
1591 Assert(8 && _delegate->pipPlaybackSpeed() >= 0.5
1592 && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash.
1593
1594 options.speed = _delegate->pipPlaybackSpeed();
1595
1596 Assert(9 && options.speed >= 0.5
1597 && options.speed <= 2.); // Debugging strange crash.
1598
1599 _instance.play(options);
1600 if (_startPaused) {
1601 _instance.pause();
1602 }
1603 _pausedBySeek = false;
1604 updatePlaybackState();
1605 }
1606
canUseVideoFrame() const1607 bool Pip::canUseVideoFrame() const {
1608 return _instance.player().ready()
1609 && !_instance.info().video.cover.isNull();
1610 }
1611
videoFrame(const FrameRequest & request) const1612 QImage Pip::videoFrame(const FrameRequest &request) const {
1613 Expects(canUseVideoFrame());
1614
1615 return _instance.frame(request);
1616 }
1617
videoFrameWithInfo() const1618 Streaming::FrameWithInfo Pip::videoFrameWithInfo() const {
1619 Expects(canUseVideoFrame());
1620
1621 return _instance.frameWithInfo();
1622 }
1623
staticContent() const1624 QImage Pip::staticContent() const {
1625 const auto &cover = _instance.info().video.cover;
1626 const auto media = _data->activeMediaView();
1627 const auto use = media
1628 ? media
1629 : _data->inlineThumbnailBytes().isEmpty()
1630 ? nullptr
1631 : _data->createMediaView();
1632 if (use) {
1633 use->goodThumbnailWanted();
1634 }
1635 const auto good = use ? use->goodThumbnail() : nullptr;
1636 const auto thumb = use ? use->thumbnail() : nullptr;
1637 const auto blurred = use ? use->thumbnailInline() : nullptr;
1638
1639 const auto state = !cover.isNull()
1640 ? ThumbState::Cover
1641 : good
1642 ? ThumbState::Good
1643 : thumb
1644 ? ThumbState::Thumb
1645 : blurred
1646 ? ThumbState::Inline
1647 : ThumbState::Empty;
1648 if (!_preparedCoverStorage.isNull() && _preparedCoverState >= state) {
1649 return _preparedCoverStorage;
1650 }
1651 _preparedCoverState = state;
1652 if (state == ThumbState::Cover) {
1653 _preparedCoverStorage = _instance.info().video.cover;
1654 } else {
1655 _preparedCoverStorage = (good
1656 ? good
1657 : thumb
1658 ? thumb
1659 : blurred
1660 ? blurred
1661 : Image::BlankMedia().get())->original();
1662 if (!good) {
1663 _preparedCoverStorage = Images::prepareBlur(
1664 std::move(_preparedCoverStorage));
1665 }
1666 }
1667 return _preparedCoverStorage;
1668 }
1669
paintRadialLoadingContent(QPainter & p,const QRect & inner,QColor fg) const1670 void Pip::paintRadialLoadingContent(
1671 QPainter &p,
1672 const QRect &inner,
1673 QColor fg) const {
1674 const auto arc = inner.marginsRemoved(QMargins(
1675 st::radialLine,
1676 st::radialLine,
1677 st::radialLine,
1678 st::radialLine));
1679 p.setOpacity(_instance.waitingOpacity());
1680 p.setPen(Qt::NoPen);
1681 p.setBrush(st::radialBg);
1682 {
1683 PainterHighQualityEnabler hq(p);
1684 p.drawEllipse(inner);
1685 }
1686 p.setOpacity(1.);
1687 Ui::InfiniteRadialAnimation::Draw(
1688 p,
1689 _instance.waitingState(),
1690 arc.topLeft(),
1691 arc.size(),
1692 _panel.widget()->width(),
1693 fg,
1694 st::radialLine);
1695 }
1696
countRadialRect() const1697 QRect Pip::countRadialRect() const {
1698 const auto outer = _panel.inner();
1699 return {
1700 outer.x() + (outer.width() - st::radialSize.width()) / 2,
1701 outer.y() + (outer.height() - st::radialSize.height()) / 2,
1702 st::radialSize.width(),
1703 st::radialSize.height()
1704 };
1705 }
1706
computeState(QPoint position) const1707 Pip::OverState Pip::computeState(QPoint position) const {
1708 if (!_panel.inner().contains(position)) {
1709 return OverState::None;
1710 } else if (_close.area.contains(position)) {
1711 return OverState::Close;
1712 } else if (_enlarge.area.contains(position)) {
1713 return OverState::Enlarge;
1714 } else if (_playback.area.contains(position)) {
1715 return OverState::Playback;
1716 } else if (_volumeToggle.area.contains(position)) {
1717 return OverState::VolumeToggle;
1718 } else if (_volumeController.area.contains(position)) {
1719 return OverState::VolumeController;
1720 } else {
1721 return OverState::Other;
1722 }
1723 }
1724
waitingAnimationCallback()1725 void Pip::waitingAnimationCallback() {
1726 _panel.widget()->update(countRadialRect());
1727 }
1728
1729 } // namespace View
1730 } // namespace Media
1731