Changeset View
Changeset View
Standalone View
Standalone View
src/screencaststream.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright © 2018 Red Hat, Inc | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or | ||||
5 | * modify it under the terms of the GNU Lesser General Public | ||||
6 | * License as published by the Free Software Foundation; either | ||||
7 | * version 2 of the License, or (at your option) any later version. | ||||
8 | * | ||||
9 | * This library is distributed in the hope that it will be useful, | ||||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||||
12 | * Lesser General Public License for more details. | ||||
13 | * | ||||
14 | * You should have received a copy of the GNU Lesser General Public | ||||
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. | ||||
16 | * | ||||
17 | * Authors: | ||||
18 | * Jan Grulich <jgrulich@redhat.com> | ||||
19 | */ | ||||
20 | | ||||
21 | #include "screencaststream.h" | ||||
22 | | ||||
23 | #include <math.h> | ||||
24 | #include <sys/mman.h> | ||||
25 | #include <stdio.h> | ||||
26 | | ||||
27 | #include <QLoggingCategory> | ||||
28 | #include <QTimer> | ||||
29 | #include <QSize> | ||||
30 | #include <QSocketNotifier> | ||||
31 | | ||||
32 | Q_LOGGING_CATEGORY(XdgDesktopPortalKdeScreenCastStream, "xdp-kde-screencast-stream") | ||||
33 | | ||||
34 | class PwFraction { | ||||
35 | public: | ||||
36 | int num; | ||||
37 | int denom; | ||||
38 | }; | ||||
39 | | ||||
40 | // Stolen from mutter | ||||
41 | | ||||
42 | #define MAX_TERMS 30 | ||||
43 | #define MIN_DIVISOR 1.0e-10 | ||||
44 | #define MAX_ERROR 1.0e-20 | ||||
45 | | ||||
46 | #define PROP_RANGE(min, max) 2, (min), (max) | ||||
47 | | ||||
48 | #define BITS_PER_PIXEL 4 | ||||
49 | | ||||
50 | static int greatestCommonDivisor(int a, int b) | ||||
51 | { | ||||
52 | while (b != 0) { | ||||
53 | int temp = a; | ||||
54 | | ||||
55 | a = b; | ||||
56 | b = temp % b; | ||||
57 | } | ||||
58 | | ||||
59 | return ABS(a); | ||||
60 | } | ||||
61 | | ||||
62 | static PwFraction pipewireFractionFromDouble(double src) | ||||
63 | { | ||||
64 | double V, F; /* double being converted */ | ||||
65 | int N, D; /* will contain the result */ | ||||
66 | int A; /* current term in continued fraction */ | ||||
67 | int64_t N1, D1; /* numerator, denominator of last approx */ | ||||
68 | int64_t N2, D2; /* numerator, denominator of previous approx */ | ||||
69 | int i; | ||||
70 | int gcd; | ||||
71 | gboolean negative = FALSE; | ||||
72 | | ||||
73 | /* initialize fraction being converted */ | ||||
74 | F = src; | ||||
75 | if (F < 0.0) { | ||||
76 | F = -F; | ||||
77 | negative = TRUE; | ||||
78 | } | ||||
79 | | ||||
80 | V = F; | ||||
81 | /* initialize fractions with 1/0, 0/1 */ | ||||
82 | N1 = 1; | ||||
83 | D1 = 0; | ||||
84 | N2 = 0; | ||||
85 | D2 = 1; | ||||
86 | N = 1; | ||||
87 | D = 1; | ||||
88 | | ||||
89 | for (i = 0; i < MAX_TERMS; i++) { | ||||
90 | /* get next term */ | ||||
91 | A = (gint) F; /* no floor() needed, F is always >= 0 */ | ||||
92 | /* get new divisor */ | ||||
93 | F = F - A; | ||||
94 | | ||||
95 | /* calculate new fraction in temp */ | ||||
96 | N2 = N1 * A + N2; | ||||
97 | D2 = D1 * A + D2; | ||||
98 | | ||||
99 | /* guard against overflow */ | ||||
100 | if (N2 > G_MAXINT || D2 > G_MAXINT) | ||||
101 | break; | ||||
102 | | ||||
103 | N = N2; | ||||
104 | D = D2; | ||||
105 | | ||||
106 | /* save last two fractions */ | ||||
107 | N2 = N1; | ||||
108 | D2 = D1; | ||||
109 | N1 = N; | ||||
110 | D1 = D; | ||||
111 | | ||||
112 | /* quit if dividing by zero or close enough to target */ | ||||
113 | if (F < MIN_DIVISOR || fabs (V - ((gdouble) N) / D) < MAX_ERROR) | ||||
114 | break; | ||||
115 | | ||||
116 | /* Take reciprocal */ | ||||
117 | F = 1 / F; | ||||
118 | } | ||||
119 | | ||||
120 | /* fix for overflow */ | ||||
121 | if (D == 0) { | ||||
122 | N = G_MAXINT; | ||||
123 | D = 1; | ||||
124 | } | ||||
125 | | ||||
126 | /* fix for negative */ | ||||
127 | if (negative) | ||||
128 | N = -N; | ||||
129 | | ||||
130 | /* simplify */ | ||||
131 | gcd = greatestCommonDivisor(N, D); | ||||
132 | if (gcd) { | ||||
133 | N /= gcd; | ||||
134 | D /= gcd; | ||||
135 | } | ||||
136 | | ||||
137 | PwFraction fraction; | ||||
138 | fraction.num = N; | ||||
139 | fraction.denom = D; | ||||
140 | | ||||
141 | return fraction; | ||||
142 | } | ||||
143 | | ||||
144 | static void onStateChanged(void *_data, pw_remote_state old, pw_remote_state state, const char *error) | ||||
145 | { | ||||
146 | Q_UNUSED(old); | ||||
147 | Q_UNUSED(_data); | ||||
148 | | ||||
149 | switch (state) { | ||||
150 | case PW_REMOTE_STATE_ERROR: | ||||
151 | // TODO notify error | ||||
152 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Remote error: " << error; | ||||
153 | break; | ||||
154 | case PW_REMOTE_STATE_CONNECTED: | ||||
155 | // TODO notify error | ||||
156 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); | ||||
157 | break; | ||||
158 | default: | ||||
159 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Remote state: " << pw_remote_state_as_string(state); | ||||
160 | break; | ||||
161 | } | ||||
162 | } | ||||
163 | | ||||
164 | static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message) | ||||
165 | { | ||||
166 | Q_UNUSED(old) | ||||
167 | | ||||
168 | ScreenCastStream *pw = static_cast<ScreenCastStream*>(data); | ||||
169 | | ||||
170 | switch (state) { | ||||
171 | case PW_STREAM_STATE_ERROR: | ||||
172 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Stream error: " << error_message; | ||||
173 | break; | ||||
174 | case PW_STREAM_STATE_CONFIGURE: | ||||
175 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); | ||||
176 | Q_EMIT pw->streamReady((uint)pw_stream_get_node_id(pw->pwStream)); | ||||
177 | break; | ||||
178 | case PW_STREAM_STATE_UNCONNECTED: | ||||
179 | case PW_STREAM_STATE_CONNECTING: | ||||
180 | case PW_STREAM_STATE_READY: | ||||
181 | case PW_STREAM_STATE_PAUSED: | ||||
182 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); | ||||
183 | Q_EMIT pw->stopStreaming(); | ||||
184 | break; | ||||
185 | case PW_STREAM_STATE_STREAMING: | ||||
186 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream state: " << pw_stream_state_as_string(state); | ||||
187 | Q_EMIT pw->startStreaming(); | ||||
188 | break; | ||||
189 | } | ||||
190 | } | ||||
191 | | ||||
192 | static void onStreamFormatChanged(void *data, struct spa_pod *format) | ||||
193 | { | ||||
194 | qCDebug(XdgDesktopPortalKdeScreenCastStream) << "Stream format changed"; | ||||
195 | | ||||
196 | ScreenCastStream *pw = static_cast<ScreenCastStream*>(data); | ||||
197 | | ||||
198 | uint8_t paramsBuffer[1024]; | ||||
199 | int32_t width, height, stride, size; | ||||
200 | struct spa_pod_builder pod_builder; | ||||
201 | struct spa_pod *params[1]; | ||||
202 | const int bpp = 4; | ||||
203 | | ||||
204 | if (!format) { | ||||
205 | pw_stream_finish_format(pw->pwStream, 0, NULL, 0); | ||||
206 | return; | ||||
207 | } | ||||
208 | | ||||
209 | spa_format_video_raw_parse (format, &pw->videoFormat, &pw->pwType->format_video); | ||||
210 | | ||||
211 | width = pw->videoFormat.size.width; | ||||
212 | height =pw->videoFormat.size.height; | ||||
213 | stride = SPA_ROUND_UP_N (width * bpp, 4); | ||||
214 | size = height * stride; | ||||
215 | | ||||
216 | pod_builder = SPA_POD_BUILDER_INIT (paramsBuffer, sizeof (paramsBuffer)); | ||||
217 | | ||||
218 | params[0] = (spa_pod*) spa_pod_builder_object (&pod_builder, | ||||
219 | pw->pwCoreType->param.idBuffers, pw->pwCoreType->param_buffers.Buffers, | ||||
220 | ":", pw->pwCoreType->param_buffers.size, "i", size, | ||||
221 | ":", pw->pwCoreType->param_buffers.stride, "i", stride, | ||||
222 | ":", pw->pwCoreType->param_buffers.buffers, "iru", 16, PROP_RANGE (2, 16), | ||||
223 | ":", pw->pwCoreType->param_buffers.align, "i", 16); | ||||
224 | | ||||
225 | pw_stream_finish_format (pw->pwStream, 0, | ||||
226 | params, G_N_ELEMENTS (params)); | ||||
227 | } | ||||
228 | | ||||
229 | static const struct pw_remote_events pwRemoteEvents = { | ||||
230 | .version = PW_VERSION_REMOTE_EVENTS, | ||||
231 | .destroy = nullptr, | ||||
232 | .info_changed = nullptr, | ||||
233 | .sync_reply = nullptr, | ||||
234 | .state_changed = onStateChanged, | ||||
235 | }; | ||||
236 | | ||||
237 | static const struct pw_stream_events pwStreamEvents = { | ||||
238 | .version = PW_VERSION_STREAM_EVENTS, | ||||
239 | .destroy = nullptr, | ||||
240 | .state_changed = onStreamStateChanged, | ||||
241 | .format_changed = onStreamFormatChanged, | ||||
242 | .add_buffer = nullptr, | ||||
243 | .remove_buffer = nullptr, | ||||
244 | .new_buffer = nullptr, | ||||
245 | .need_buffer = nullptr, | ||||
246 | }; | ||||
247 | | ||||
248 | ScreenCastStream::ScreenCastStream(QObject *parent) | ||||
249 | : QObject(parent) | ||||
250 | { | ||||
251 | } | ||||
252 | | ||||
253 | ScreenCastStream::~ScreenCastStream() | ||||
254 | { | ||||
255 | if (pwType) { | ||||
256 | delete pwType; | ||||
257 | } | ||||
258 | | ||||
259 | if (pwCore) { | ||||
260 | pw_core_destroy(pwCore); | ||||
261 | } | ||||
262 | | ||||
263 | if (pwLoop) { | ||||
264 | pw_loop_leave(pwLoop); | ||||
265 | pw_loop_destroy(pwLoop); | ||||
266 | } | ||||
267 | } | ||||
268 | | ||||
269 | void ScreenCastStream::init() | ||||
270 | { | ||||
271 | pw_init(nullptr, nullptr); | ||||
272 | | ||||
273 | pwLoop = pw_loop_new(nullptr); | ||||
274 | socketNotifier.reset(new QSocketNotifier(pw_loop_get_fd(pwLoop), QSocketNotifier::Read)); | ||||
275 | connect(socketNotifier.data(), &QSocketNotifier::activated, this, &ScreenCastStream::processPipewireEvents); | ||||
276 | | ||||
277 | pwCore = pw_core_new(pwLoop, nullptr); | ||||
278 | pwCoreType = pw_core_get_type(pwCore); | ||||
279 | pwRemote = pw_remote_new(pwCore, nullptr, 0); | ||||
280 | | ||||
281 | spa_debug_set_type_map(pwCoreType->map); | ||||
282 | | ||||
283 | initializePwTypes(); | ||||
284 | | ||||
285 | pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); | ||||
286 | | ||||
287 | pw_remote_connect(pwRemote); | ||||
288 | } | ||||
289 | | ||||
290 | uint ScreenCastStream::nodeId() | ||||
291 | { | ||||
292 | if (pwStream) { | ||||
293 | return (uint)pw_stream_get_node_id(pwStream); | ||||
294 | } | ||||
295 | | ||||
296 | return 0; | ||||
297 | } | ||||
298 | | ||||
299 | bool ScreenCastStream::createStream(const QSize &resolution) | ||||
300 | { | ||||
301 | if (pw_remote_get_state(pwRemote, nullptr) != PW_REMOTE_STATE_CONNECTED) { | ||||
302 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Cannot create pipewire stream"; | ||||
303 | return false; | ||||
304 | } | ||||
305 | | ||||
306 | uint8_t buffer[1024]; | ||||
307 | spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); | ||||
308 | | ||||
309 | const float frameRate = 25; | ||||
310 | | ||||
311 | spa_fraction maxFramerate; | ||||
312 | spa_fraction minFramerate; | ||||
313 | const spa_pod *params[1]; | ||||
314 | | ||||
315 | pwStream = pw_stream_new(pwRemote, "kwin-screen-cast", nullptr); | ||||
316 | | ||||
317 | PwFraction fraction = pipewireFractionFromDouble(frameRate); | ||||
318 | | ||||
319 | minFramerate = SPA_FRACTION(1, 1); | ||||
320 | maxFramerate = SPA_FRACTION((uint32_t)fraction.num, (uint32_t)fraction.denom); | ||||
321 | | ||||
322 | spa_fraction paramFraction = SPA_FRACTION(0, 1); | ||||
323 | spa_rectangle paramRectangle = SPA_RECTANGLE((uint32_t)resolution.width(), (uint32_t)resolution.height()); | ||||
324 | | ||||
325 | params[0] = (spa_pod*)spa_pod_builder_object(&podBuilder, | ||||
326 | pwCoreType->param.idEnumFormat, pwCoreType->spa_format, | ||||
327 | "I", pwType->media_type.video, | ||||
328 | "I", pwType->media_subtype.raw, | ||||
329 | ":", pwType->format_video.format, "I", pwType->video_format.RGBx, | ||||
330 | ":", pwType->format_video.size, "R", ¶mRectangle, | ||||
331 | ":", pwType->format_video.framerate, "F", ¶mFraction, | ||||
332 | ":", pwType->format_video.max_framerate, "Fr", &maxFramerate, PROP_RANGE (&minFramerate, &maxFramerate)); | ||||
333 | | ||||
334 | pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); | ||||
335 | | ||||
336 | if (pw_stream_connect(pwStream, PW_DIRECTION_OUTPUT, nullptr, PW_STREAM_FLAG_NONE, params, G_N_ELEMENTS(¶ms)) != 0) { | ||||
337 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Could not connect to stream"; | ||||
338 | return false; | ||||
339 | } | ||||
340 | | ||||
341 | return true; | ||||
342 | } | ||||
343 | | ||||
344 | bool ScreenCastStream::recordFrame(uint8_t *screenData) | ||||
345 | { | ||||
346 | uint32_t bufferId; | ||||
347 | struct spa_buffer *buffer; | ||||
348 | uint8_t *map = nullptr; | ||||
349 | uint8_t *data = nullptr; | ||||
350 | | ||||
351 | // TODO check timestamp like mutter does? | ||||
352 | | ||||
353 | if (!pwStream) { | ||||
354 | return false; | ||||
355 | } | ||||
356 | | ||||
357 | bufferId = pw_stream_get_empty_buffer(pwStream); | ||||
358 | | ||||
359 | if (bufferId == SPA_ID_INVALID) { | ||||
360 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to get empty stream buffer: " << strerror(errno); | ||||
361 | return false; | ||||
362 | } | ||||
363 | | ||||
364 | buffer = pw_stream_peek_buffer(pwStream, bufferId); | ||||
365 | | ||||
366 | if (buffer->datas[0].type == pwCoreType->data.MemFd) { | ||||
367 | map = (uint8_t *)mmap(nullptr, buffer->datas[0].maxsize + buffer->datas[0].mapoffset, PROT_READ | PROT_WRITE, MAP_SHARED, buffer->datas[0].fd, 0); | ||||
368 | | ||||
369 | if (map == MAP_FAILED) { | ||||
370 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to mmap pipewire stream buffer: " << strerror(errno); | ||||
371 | return false; | ||||
372 | } | ||||
373 | | ||||
374 | data = SPA_MEMBER(map, buffer->datas[0].mapoffset, uint8_t); | ||||
375 | } else if (buffer->datas[0].type == pwCoreType->data.MemPtr) { | ||||
376 | data = (uint8_t *) buffer->datas[0].data; | ||||
377 | } else { | ||||
378 | return false; | ||||
379 | } | ||||
380 | | ||||
381 | memcpy(data, screenData, BITS_PER_PIXEL * videoFormat.size.height * videoFormat.size.width * sizeof(uint8_t)); | ||||
382 | | ||||
383 | if (map) { | ||||
384 | munmap(map, buffer->datas[0].maxsize + buffer->datas[0].mapoffset); | ||||
385 | } | ||||
386 | | ||||
387 | buffer->datas[0].chunk->size = buffer->datas[0].maxsize; | ||||
388 | | ||||
389 | pw_stream_send_buffer(pwStream, bufferId); | ||||
390 | | ||||
391 | return true; | ||||
392 | } | ||||
393 | | ||||
394 | void ScreenCastStream::removeStream() | ||||
395 | { | ||||
396 | pw_stream_destroy(pwStream); | ||||
397 | pwStream = nullptr; | ||||
398 | } | ||||
399 | | ||||
400 | void ScreenCastStream::initializePwTypes() | ||||
401 | { | ||||
402 | // raw C-like ScreenCastStream type map | ||||
403 | auto map = pwCoreType->map; | ||||
404 | | ||||
405 | pwType = new PwType(); | ||||
406 | | ||||
407 | spa_type_media_type_map(map, &pwType->media_type); | ||||
408 | spa_type_media_subtype_map(map, &pwType->media_subtype); | ||||
409 | spa_type_format_video_map (map, &pwType->format_video); | ||||
410 | spa_type_video_format_map (map, &pwType->video_format); | ||||
411 | } | ||||
412 | | ||||
413 | void ScreenCastStream::processPipewireEvents() | ||||
414 | { | ||||
415 | int result = pw_loop_iterate(pwLoop, 0); | ||||
416 | if (result < 0) { | ||||
417 | qCWarning(XdgDesktopPortalKdeScreenCastStream) << "Failed to iterate over pipewire loop: " << spa_strerror(result); | ||||
418 | } | ||||
419 | } |