summaryrefslogtreecommitdiffstats
path: root/pica-client/videoscaler.cpp
blob: 676a78fa15da9a4c6418f2a5341fc2ec6746fbff (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
	(c) Copyright  2012 - 2026 Anton Sviridenko
	https://picapica.im

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, version 3.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "videoscaler.h"

#include <QDebug>

// FFmpeg headers
extern "C" {
#include <libavutil/pixfmt.h>
#include <libswscale/swscale.h>
}

// The QImage formats libswscale can be pointed at directly. Anything missing
// here is not an error - scale() just uses Qt for those frames. Premultiplied
// formats are deliberately absent: libswscale has no equivalent, and feeding
// it premultiplied pixels as if they were plain ones would wash the colours
// out.
static AVPixelFormat av_pixel_format(QImage::Format format)
{
	switch (format)
	{
	case QImage::Format_RGB888:
		return AV_PIX_FMT_RGB24;
	case QImage::Format_RGB32:
		return AV_PIX_FMT_0RGB32;
	case QImage::Format_ARGB32:
		return AV_PIX_FMT_RGB32;
	case QImage::Format_RGBX8888:
		return AV_PIX_FMT_RGB0;
	case QImage::Format_RGBA8888:
		return AV_PIX_FMT_RGBA;
	case QImage::Format_Grayscale8:
		return AV_PIX_FMT_GRAY8;
	default:
		return AV_PIX_FMT_NONE;
	}
}

VideoScaler::VideoScaler(Backend backend)
	: m_backend(backend), m_lastUsed(backend), m_sws(nullptr),
	  m_srcWidth(0), m_srcHeight(0), m_srcFormat(AV_PIX_FMT_NONE),
	  m_dstWidth(0), m_dstHeight(0), m_loggedFailure(false)
{
}

VideoScaler::~VideoScaler()
{
	if (m_sws)
		sws_freeContext(m_sws);
}

QSize VideoScaler::fitted(QSize frame, QSize area)
{
	if (frame.isEmpty() || area.isEmpty())
		return QSize();

	QSize result = frame;
	result.scale(area, Qt::KeepAspectRatio);

	// An odd width costs libswscale a factor of four and a half, because it
	// drops off its SIMD path and converts the picture a pixel at a time -
	// measured, and far and away the largest effect of any of the geometry
	// here: destination stride alignment and an odd height both turned out not
	// to matter at all. Window widths being whatever the user drags them to,
	// half of them would land on the slow path.
	//
	// So the width is rounded down to even. That loses at most one pixel of
	// picture and bends the aspect ratio by well under a tenth of a percent,
	// which is less than the rounding above has already done.
	//
	// It matters that this is the one place the target size is decided.
	// VideoDevice::Play() sizes the frames it produces by it and CallWindow
	// checks arriving frames against it - if the two disagreed by a pixel, the
	// window would resize every frame instead of none of them.
	result.setWidth(result.width() & ~1);

	// A picture far thinner than the area it goes into can round down to
	// nothing, and neither scaler accepts a zero sized target.
	if (result.width() < 2)
		result.setWidth(2);
	if (result.height() < 1)
		result.setHeight(1);

	return result;
}

QImage VideoScaler::scale(const QImage &frame, QSize area)
{
	QSize target = fitted(frame.size(), area);

	if (target.isEmpty())
		return QImage();

	if (m_backend == Swscale)
	{
		QImage scaled = scaleWithSwscale(frame, target);

		if (!scaled.isNull())
		{
			m_lastUsed = Swscale;
			return scaled;
		}
	}

	m_lastUsed = QtScaled;
	return scaleWithQt(frame, target);
}

QImage VideoScaler::scaleWithSwscale(const QImage &frame, QSize target)
{
	AVPixelFormat srcFormat = av_pixel_format(frame.format());

	if (srcFormat == AV_PIX_FMT_NONE)
		return QImage();

	if (!m_sws || frame.width() != m_srcWidth || frame.height() != m_srcHeight ||
	    (int)srcFormat != m_srcFormat || target.width() != m_dstWidth ||
	    target.height() != m_dstHeight)
	{
		if (m_sws)
		{
			sws_freeContext(m_sws);
			m_sws = nullptr;
		}

		// SWS_BILINEAR to match what Qt's SmoothTransformation looks
		// like, and what the playback path already converts colours
		// with in videodevice.cpp.
		m_sws = sws_getContext(frame.width(), frame.height(), srcFormat,
		                       target.width(), target.height(), AV_PIX_FMT_0RGB32,
		                       SWS_BILINEAR, nullptr, nullptr, nullptr);

		if (!m_sws)
		{
			if (!m_loggedFailure)
			{
				qWarning() << "Could not set up video display scaler, using Qt instead";
				m_loggedFailure = true;
			}
			return QImage();
		}

		m_srcWidth = frame.width();
		m_srcHeight = frame.height();
		m_srcFormat = (int)srcFormat;
		m_dstWidth = target.width();
		m_dstHeight = target.height();

		// AV_PIX_FMT_0RGB32 is defined to be exactly this layout on
		// either endianness, so libswscale can write into the image
		// without a conversion in between - and Qt draws a 32 bit
		// image without one either.
		m_dst = QImage(target.width(), target.height(), QImage::Format_RGB32);

		if (m_dst.isNull())
			return QImage();
	}

	const uint8_t *srcData[4] = { frame.constBits(), nullptr, nullptr, nullptr };
	int srcStride[4] = { (int)frame.bytesPerLine(), 0, 0, 0 };

	// bits() would deep copy if m_dst were shared with anyone, which is why
	// what scale() hands back only points at this buffer instead of sharing
	// it.
	uint8_t *dstData[4] = { m_dst.bits(), nullptr, nullptr, nullptr };
	int dstStride[4] = { (int)m_dst.bytesPerLine(), 0, 0, 0 };

	if (sws_scale(m_sws, srcData, srcStride, 0, frame.height(), dstData, dstStride) <= 0)
		return QImage();

	return QImage(m_dst.constBits(), m_dst.width(), m_dst.height(),
	              m_dst.bytesPerLine(), m_dst.format());
}

QImage VideoScaler::scaleWithQt(const QImage &frame, QSize target)
{
	// The aspect ratio is already accounted for in target, so scaling to it
	// exactly avoids rounding the size a second time.
	return frame.scaled(target, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}