268 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			268 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "calls/calls_video_bubble.h"
 | |
| 
 | |
| #include "webrtc/webrtc_video_track.h"
 | |
| #include "ui/image/image_prepare.h"
 | |
| #include "ui/widgets/shadow.h"
 | |
| #include "styles/style_calls.h"
 | |
| #include "styles/style_widgets.h"
 | |
| #include "styles/style_layers.h"
 | |
| 
 | |
| namespace Calls {
 | |
| 
 | |
| VideoBubble::VideoBubble(
 | |
| 	not_null<QWidget*> parent,
 | |
| 	not_null<Webrtc::VideoTrack*> track)
 | |
| : _content(parent)
 | |
| , _track(track)
 | |
| , _state(Webrtc::VideoState::Inactive) {
 | |
| 	setup();
 | |
| }
 | |
| 
 | |
| void VideoBubble::setup() {
 | |
| 	_content.show();
 | |
| 	applyDragMode(_dragMode);
 | |
| 
 | |
| 	_content.paintRequest(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		paint();
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	_track->stateValue(
 | |
| 	) | rpl::start_with_next([=](Webrtc::VideoState state) {
 | |
| 		setState(state);
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	_track->renderNextFrame(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		if (_track->frameSize().isEmpty()) {
 | |
| 			_track->markFrameShown();
 | |
| 		} else {
 | |
| 			updateVisibility();
 | |
| 			// We update whole parent widget in this case.
 | |
| 			// In case we update only bubble without the parent incoming
 | |
| 			// video frame we may get full parent of old frame with a
 | |
| 			// rectangular piece of a new frame rendered with that update().
 | |
| 			//_content.update();
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| }
 | |
| 
 | |
| void VideoBubble::updateGeometry(
 | |
| 		DragMode mode,
 | |
| 		QRect boundingRect,
 | |
| 		QSize sizeMin,
 | |
| 		QSize sizeMax) {
 | |
| 	Expects(!boundingRect.isEmpty());
 | |
| 	Expects(sizeMax.isEmpty() || !sizeMin.isEmpty());
 | |
| 	Expects(sizeMax.isEmpty() || sizeMin.width() <= sizeMax.width());
 | |
| 	Expects(sizeMax.isEmpty() || sizeMin.height() <= sizeMax.height());
 | |
| 
 | |
| 	if (sizeMin.isEmpty()) {
 | |
| 		sizeMin = boundingRect.size();
 | |
| 	}
 | |
| 	if (sizeMax.isEmpty()) {
 | |
| 		sizeMax = sizeMin;
 | |
| 	}
 | |
| 	if (_dragMode != mode) {
 | |
| 		applyDragMode(mode);
 | |
| 	}
 | |
| 	if (_boundingRect != boundingRect) {
 | |
| 		applyBoundingRect(boundingRect);
 | |
| 	}
 | |
| 	if (_min != sizeMin || _max != sizeMax) {
 | |
| 		applySizeConstraints(sizeMin, sizeMax);
 | |
| 	}
 | |
| 	if (_geometryDirty && !_lastFrameSize.isEmpty()) {
 | |
| 		updateSizeToFrame(base::take(_lastFrameSize));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| QRect VideoBubble::geometry() const {
 | |
| 	return _content.isHidden() ? QRect() : _content.geometry();
 | |
| }
 | |
| 
 | |
| void VideoBubble::applyBoundingRect(QRect rect) {
 | |
| 	_boundingRect = rect;
 | |
| 	_geometryDirty = true;
 | |
| }
 | |
| 
 | |
| void VideoBubble::applyDragMode(DragMode mode) {
 | |
| 	_dragMode = mode;
 | |
| 	if (_dragMode == DragMode::None) {
 | |
| 		_dragging = false;
 | |
| 		_content.setCursor(style::cur_default);
 | |
| 	}
 | |
| 	_content.setAttribute(
 | |
| 		Qt::WA_TransparentForMouseEvents,
 | |
| 		true/*(_dragMode == DragMode::None)*/);
 | |
| 	if (_dragMode == DragMode::SnapToCorners) {
 | |
| 		_corner = RectPart::BottomRight;
 | |
| 	} else {
 | |
| 		_corner = RectPart::None;
 | |
| 		_lastDraggableSize = _size;
 | |
| 	}
 | |
| 	_size = QSize();
 | |
| 	_geometryDirty = true;
 | |
| }
 | |
| 
 | |
| void VideoBubble::applySizeConstraints(QSize min, QSize max) {
 | |
| 	_min = min;
 | |
| 	_max = max;
 | |
| 	_geometryDirty = true;
 | |
| }
 | |
| 
 | |
| void VideoBubble::paint() {
 | |
| 	auto p = QPainter(&_content);
 | |
| 
 | |
| 	prepareFrame();
 | |
| 	if (!_frame.isNull()) {
 | |
| 		const auto padding = st::boxRoundShadow.extend;
 | |
| 		const auto inner = _content.rect().marginsRemoved(padding);
 | |
| 		Ui::Shadow::paint(p, inner, _content.width(), st::boxRoundShadow);
 | |
| 		const auto factor = cIntRetinaFactor();
 | |
| 		p.drawImage(
 | |
| 			inner,
 | |
| 			_frame,
 | |
| 			QRect(
 | |
| 				QPoint(_frame.width() - (inner.width() * factor), 0),
 | |
| 				inner.size() * factor));
 | |
| 	}
 | |
| 	_track->markFrameShown();
 | |
| }
 | |
| 
 | |
| void VideoBubble::prepareFrame() {
 | |
| 	const auto original = _track->frameSize();
 | |
| 	if (original.isEmpty()) {
 | |
| 		_frame = QImage();
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto padding = st::boxRoundShadow.extend;
 | |
| 	const auto size = _content.rect().marginsRemoved(padding).size()
 | |
| 		* cIntRetinaFactor();
 | |
| 
 | |
| 	// Should we check 'original' and 'size' aspect ratios?..
 | |
| 	const auto request = Webrtc::FrameRequest{
 | |
| 		.resize = size,
 | |
| 		.outer = size,
 | |
| 	};
 | |
| 	const auto frame = _track->frame(request).mirrored(!_mirrored, false);
 | |
| 	if (_frame.width() < size.width() || _frame.height() < size.height()) {
 | |
| 		_frame = QImage(
 | |
| 			size * cIntRetinaFactor(),
 | |
| 			QImage::Format_ARGB32_Premultiplied);
 | |
| 	}
 | |
| 	Assert(_frame.width() >= frame.width()
 | |
| 		&& _frame.height() >= frame.height());
 | |
| 	const auto dstPerLine = _frame.bytesPerLine();
 | |
| 	const auto srcPerLine = frame.bytesPerLine();
 | |
| 	const auto lineSize = frame.width() * 4;
 | |
| 	auto dst = _frame.bits();
 | |
| 	auto src = frame.bits();
 | |
| 	const auto till = src + frame.height() * srcPerLine;
 | |
| 	for (; src != till; src += srcPerLine, dst += dstPerLine) {
 | |
| 		memcpy(dst, src, lineSize);
 | |
| 	}
 | |
| 	_frame = Images::Round(
 | |
| 		std::move(_frame),
 | |
| 		ImageRoundRadius::Large,
 | |
| 		RectPart::AllCorners,
 | |
| 		QRect(QPoint(), size)
 | |
| 	).mirrored(true, false);
 | |
| }
 | |
| 
 | |
| void VideoBubble::setState(Webrtc::VideoState state) {
 | |
| 	if (state == Webrtc::VideoState::Paused) {
 | |
| 		using namespace Images;
 | |
| 		static constexpr auto kRadius = 24;
 | |
| 		_pausedFrame = Images::BlurLargeImage(_track->frame({}), kRadius);
 | |
| 		if (_pausedFrame.isNull()) {
 | |
| 			state = Webrtc::VideoState::Inactive;
 | |
| 		}
 | |
| 	}
 | |
| 	_state = state;
 | |
| 	updateVisibility();
 | |
| }
 | |
| 
 | |
| void VideoBubble::updateSizeToFrame(QSize frame) {
 | |
| 	Expects(!frame.isEmpty());
 | |
| 
 | |
| 	if (_lastFrameSize == frame) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_lastFrameSize = frame;
 | |
| 
 | |
| 	auto size = !_size.isEmpty()
 | |
| 		? QSize(
 | |
| 			std::clamp(_size.width(), _min.width(), _max.width()),
 | |
| 			std::clamp(_size.height(), _min.height(), _max.height()))
 | |
| 		: (_dragMode == DragMode::None || _lastDraggableSize.isEmpty())
 | |
| 		? QSize()
 | |
| 		: _lastDraggableSize;
 | |
| 	if (size.isEmpty()) {
 | |
| 		size = frame.scaled((_min + _max) / 2, Qt::KeepAspectRatio);
 | |
| 	} else {
 | |
| 		const auto area = size.width() * size.height();
 | |
| 		const auto w = int(base::SafeRound(std::max(
 | |
| 			std::sqrt((frame.width() * float64(area)) / (frame.height() * 1.)),
 | |
| 			1.)));
 | |
| 		const auto h = area / w;
 | |
| 		size = QSize(w, h);
 | |
| 		if (w > _max.width() || h > _max.height()) {
 | |
| 			size = size.scaled(_max, Qt::KeepAspectRatio);
 | |
| 		}
 | |
| 	}
 | |
| 	size = QSize(std::max(1, size.width()), std::max(1, size.height()));
 | |
| 	setInnerSize(size);
 | |
| }
 | |
| 
 | |
| void VideoBubble::setInnerSize(QSize size) {
 | |
| 	if (_size == size && !_geometryDirty) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_geometryDirty = false;
 | |
| 	_size = size;
 | |
| 	const auto topLeft = [&] {
 | |
| 		switch (_corner) {
 | |
| 		case RectPart::None:
 | |
| 			return _boundingRect.topLeft() + QPoint(
 | |
| 				(_boundingRect.width() - size.width()) / 2,
 | |
| 				(_boundingRect.height() - size.height()) / 2);
 | |
| 		case RectPart::TopLeft:
 | |
| 			return _boundingRect.topLeft();
 | |
| 		case RectPart::TopRight:
 | |
| 			return QPoint(
 | |
| 				_boundingRect.x() + _boundingRect.width() - size.width(),
 | |
| 				_boundingRect.y());
 | |
| 		case RectPart::BottomRight:
 | |
| 			return QPoint(
 | |
| 				_boundingRect.x() + _boundingRect.width() - size.width(),
 | |
| 				_boundingRect.y() + _boundingRect.height() - size.height());
 | |
| 		case RectPart::BottomLeft:
 | |
| 			return QPoint(
 | |
| 				_boundingRect.x(),
 | |
| 				_boundingRect.y() + _boundingRect.height() - size.height());
 | |
| 		}
 | |
| 		Unexpected("Corner value in VideoBubble::setInnerSize.");
 | |
| 	}();
 | |
| 	const auto inner = QRect(topLeft, size);
 | |
| 	_content.setGeometry(inner.marginsAdded(st::boxRoundShadow.extend));
 | |
| }
 | |
| 
 | |
| void VideoBubble::updateVisibility() {
 | |
| 	const auto size = _track->frameSize();
 | |
| 	const auto visible = (_state != Webrtc::VideoState::Inactive)
 | |
| 		&& !size.isEmpty();
 | |
| 	if (visible) {
 | |
| 		updateSizeToFrame(size);
 | |
| 	}
 | |
| 	_content.setVisible(visible);
 | |
| }
 | |
| 
 | |
| } // namespace Calls
 | 
