Line data Source code
1 : /*
2 : * Copyright (C) 2023 Sky UK
3 : *
4 : * This library 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;
7 : * version 2.1 of the License.
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, write to the Free Software
16 : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 : */
18 :
19 : #include "GStreamerWebAudioPlayerClient.h"
20 : #include "GstreamerCatLog.h"
21 :
22 : #include <string.h>
23 :
24 : #include <algorithm>
25 : #include <chrono>
26 : #include <cstdlib>
27 : #include <thread>
28 :
29 : #define GST_CAT_DEFAULT rialtoGStreamerCat
30 :
31 : namespace
32 : {
33 : constexpr std::size_t kMaxQueueSize{40};
34 67 : bool parseGstStructureFormat(const std::string &format, uint32_t &sampleSize, bool &isBigEndian, bool &isSigned,
35 : bool &isFloat)
36 : {
37 67 : if (format.size() != 5)
38 : {
39 1 : return false;
40 : }
41 66 : std::string sampleSizeStr = format.substr(1, 2);
42 66 : char *pEnd = NULL;
43 66 : errno = 0;
44 66 : sampleSize = strtoul(sampleSizeStr.c_str(), &pEnd, 10);
45 66 : if (errno == ERANGE)
46 : {
47 0 : return false;
48 : }
49 :
50 66 : isBigEndian = format.substr(3) == "BE";
51 :
52 66 : switch (format[0])
53 : {
54 61 : case 'S':
55 61 : isSigned = true;
56 61 : isFloat = false;
57 61 : break;
58 3 : case 'U':
59 3 : isSigned = false;
60 3 : isFloat = false;
61 3 : break;
62 1 : case 'F':
63 1 : isSigned = false;
64 1 : isFloat = true;
65 1 : break;
66 1 : default:
67 1 : return false;
68 : break;
69 : }
70 65 : return true;
71 66 : }
72 :
73 2 : bool operator!=(const firebolt::rialto::WebAudioPcmConfig &lac, const firebolt::rialto::WebAudioPcmConfig &rac)
74 : {
75 2 : return lac.rate != rac.rate || lac.channels != rac.channels || lac.sampleSize != rac.sampleSize ||
76 4 : lac.isBigEndian != rac.isBigEndian || lac.isSigned != rac.isSigned || lac.isFloat != rac.isFloat;
77 : }
78 : } // namespace
79 :
80 88 : GStreamerWebAudioPlayerClient::GStreamerWebAudioPlayerClient(
81 : std::unique_ptr<firebolt::rialto::client::WebAudioClientBackendInterface> &&webAudioClientBackend,
82 : std::unique_ptr<IMessageQueue> &&backendQueue, IPlaybackDelegate &delegate,
83 88 : std::shared_ptr<ITimerFactory> timerFactory)
84 88 : : m_backendQueue{std::move(backendQueue)}, m_clientBackend{std::move(webAudioClientBackend)}, m_isOpen{false},
85 88 : m_dataBuffers{}, m_timerFactory{timerFactory}, m_pushSamplesTimer{nullptr}, m_preferredFrames{0},
86 88 : m_maximumFrames{0}, m_supportDeferredPlay{false}, m_isEos{false}, m_frameSize{0}, m_mimeType{}, m_config{{}},
87 176 : m_delegate{delegate}
88 : {
89 88 : m_backendQueue->start();
90 : }
91 :
92 88 : GStreamerWebAudioPlayerClient::~GStreamerWebAudioPlayerClient()
93 : {
94 88 : m_backendQueue->stop();
95 : }
96 :
97 73 : bool GStreamerWebAudioPlayerClient::open(GstCaps *caps)
98 : {
99 73 : GST_DEBUG("entry:");
100 :
101 73 : bool result = false;
102 73 : GstStructure *structure = gst_caps_get_structure(caps, 0);
103 73 : std::string audioMimeType = gst_structure_get_name(structure);
104 73 : const auto spacePosition = audioMimeType.find(' ');
105 73 : if (spacePosition != std::string::npos)
106 : {
107 0 : audioMimeType.resize(spacePosition);
108 : }
109 73 : const gchar *formatCStr{gst_structure_get_string(structure, "format")};
110 146 : std::string format{formatCStr ? formatCStr : ""};
111 : firebolt::rialto::WebAudioPcmConfig pcm;
112 : gint tmp;
113 :
114 73 : if (format.empty())
115 : {
116 4 : GST_ERROR("Format not found in caps");
117 4 : return result;
118 : }
119 :
120 69 : if (!gst_structure_get_int(structure, "rate", &tmp))
121 : {
122 1 : GST_ERROR("Rate not found in caps");
123 1 : return result;
124 : }
125 68 : pcm.rate = tmp;
126 :
127 68 : if (!gst_structure_get_int(structure, "channels", &tmp))
128 : {
129 1 : GST_ERROR("Rate not found in caps");
130 1 : return result;
131 : }
132 67 : pcm.channels = tmp;
133 :
134 67 : if (!parseGstStructureFormat(format, pcm.sampleSize, pcm.isBigEndian, pcm.isSigned, pcm.isFloat))
135 : {
136 2 : GST_ERROR("Can't parse format or it is not supported: %s", format.c_str());
137 2 : return result;
138 : }
139 :
140 130 : m_backendQueue->callInEventLoop(
141 65 : [&]()
142 : {
143 : // "configWorkaround" is used because there doesn't seem to be an easy way to use
144 : // make_shared in conjunction with an initalizer list, and adding a constructor to
145 : // WebAudioConfig stops initalizer lists (used elsewhere in the code) from working...
146 65 : firebolt::rialto::WebAudioConfig configWorkaround{pcm};
147 : std::shared_ptr<firebolt::rialto::WebAudioConfig> config =
148 65 : std::make_shared<firebolt::rialto::WebAudioConfig>(configWorkaround);
149 :
150 : // Only recreate player if the config has changed
151 65 : if (!m_isOpen || isNewConfig(audioMimeType, config))
152 : {
153 64 : if (m_isOpen)
154 : {
155 : // Destroy the previously created player
156 3 : m_clientBackend->destroyWebAudioBackend();
157 : }
158 :
159 64 : uint32_t priority = 1;
160 64 : if (m_clientBackend->createWebAudioBackend(shared_from_this(), audioMimeType, priority, config))
161 : {
162 63 : if (!m_clientBackend->getDeviceInfo(m_preferredFrames, m_maximumFrames, m_supportDeferredPlay))
163 : {
164 1 : GST_ERROR("GetDeviceInfo failed, could not process samples");
165 : }
166 63 : m_frameSize = (pcm.sampleSize * pcm.channels) / CHAR_BIT;
167 63 : m_isOpen = true;
168 :
169 : // Store config
170 63 : m_config.pcm = pcm;
171 63 : m_mimeType = audioMimeType;
172 : }
173 : else
174 : {
175 1 : GST_ERROR("Could not create web audio backend");
176 1 : m_isOpen = false;
177 : }
178 64 : result = m_isOpen;
179 : }
180 65 : });
181 :
182 65 : return result;
183 73 : }
184 :
185 37 : bool GStreamerWebAudioPlayerClient::close()
186 : {
187 37 : GST_DEBUG("entry:");
188 :
189 74 : m_backendQueue->callInEventLoop(
190 37 : [&]()
191 : {
192 37 : m_clientBackend->destroyWebAudioBackend();
193 37 : m_pushSamplesTimer.reset();
194 37 : m_isOpen = false;
195 37 : });
196 :
197 37 : return true;
198 : }
199 :
200 21 : bool GStreamerWebAudioPlayerClient::play()
201 : {
202 21 : GST_DEBUG("entry:");
203 :
204 21 : bool result = false;
205 42 : m_backendQueue->callInEventLoop(
206 21 : [&]()
207 : {
208 21 : if (m_isOpen)
209 : {
210 20 : result = m_clientBackend->play();
211 : }
212 : else
213 : {
214 1 : GST_ERROR("No web audio backend");
215 : }
216 21 : });
217 :
218 21 : return result;
219 : }
220 :
221 21 : bool GStreamerWebAudioPlayerClient::pause()
222 : {
223 21 : GST_DEBUG("entry:");
224 :
225 21 : bool result = false;
226 42 : m_backendQueue->callInEventLoop(
227 21 : [&]()
228 : {
229 21 : if (m_isOpen)
230 : {
231 20 : result = m_clientBackend->pause();
232 : }
233 : else
234 : {
235 1 : GST_ERROR("No web audio backend");
236 : }
237 21 : });
238 :
239 21 : return result;
240 : }
241 :
242 8 : bool GStreamerWebAudioPlayerClient::setEos()
243 : {
244 8 : GST_DEBUG("entry:");
245 :
246 8 : bool result = false;
247 16 : m_backendQueue->callInEventLoop(
248 8 : [&]()
249 : {
250 8 : if (m_isOpen && !m_isEos)
251 : {
252 6 : m_isEos = true;
253 6 : if (m_dataBuffers.empty())
254 : {
255 5 : result = m_clientBackend->setEos();
256 : }
257 : else
258 : {
259 1 : pushSamples();
260 1 : result = true;
261 : }
262 : }
263 : else
264 : {
265 2 : GST_DEBUG("No web audio backend, valid scenario");
266 : }
267 8 : });
268 :
269 8 : return result;
270 : }
271 :
272 41 : bool GStreamerWebAudioPlayerClient::isOpen()
273 : {
274 41 : GST_DEBUG("entry:");
275 :
276 41 : bool result = false;
277 82 : m_backendQueue->callInEventLoop([&]() { result = m_isOpen; });
278 :
279 41 : return result;
280 : }
281 :
282 2 : void GStreamerWebAudioPlayerClient::notifyPushSamplesTimerExpired()
283 : {
284 4 : m_backendQueue->scheduleInEventLoop([&]() { pushSamples(); });
285 2 : }
286 :
287 10 : bool GStreamerWebAudioPlayerClient::notifyNewSample(GstBuffer *buf)
288 : {
289 10 : GST_DEBUG("entry:");
290 :
291 10 : bool result = false;
292 :
293 : {
294 10 : std::unique_lock lock{m_queueSizeMutex};
295 20 : m_queueSizeCv.wait(lock, [&]() { return m_dataBuffers.size() < kMaxQueueSize; });
296 10 : }
297 :
298 20 : m_backendQueue->callInEventLoop(
299 10 : [&]()
300 : {
301 10 : if (buf)
302 : {
303 10 : if (m_pushSamplesTimer)
304 : {
305 1 : m_pushSamplesTimer->cancel();
306 1 : m_pushSamplesTimer.reset();
307 : }
308 : {
309 10 : std::unique_lock lock{m_queueSizeMutex};
310 10 : m_dataBuffers.push(buf);
311 : }
312 10 : pushSamples();
313 10 : result = true;
314 : }
315 10 : });
316 :
317 10 : return result;
318 : }
319 :
320 13 : void GStreamerWebAudioPlayerClient::pushSamples()
321 : {
322 13 : GST_DEBUG("entry:");
323 13 : if (!m_isOpen || m_dataBuffers.empty())
324 : {
325 1 : return;
326 : }
327 :
328 12 : uint32_t availableFrames = 0u;
329 : do
330 : {
331 15 : if (!m_clientBackend->getBufferAvailable(availableFrames))
332 : {
333 4 : GST_ERROR("getBufferAvailable failed, could not process the samples");
334 : // clear the queue if getBufferAvailable failed
335 4 : std::queue<GstBuffer *> empty;
336 4 : std::unique_lock lock{m_queueSizeMutex};
337 4 : std::swap(m_dataBuffers, empty);
338 4 : m_queueSizeCv.notify_one();
339 : }
340 11 : else if (0 != availableFrames)
341 : {
342 6 : bool writeFailure = false;
343 6 : GstBuffer *buffer = m_dataBuffers.front();
344 6 : gsize bufferSize = gst_buffer_get_size(buffer);
345 6 : auto framesToWrite = std::min(availableFrames, static_cast<uint32_t>(bufferSize / m_frameSize));
346 6 : if (framesToWrite > 0)
347 : {
348 : GstMapInfo bufferMap;
349 4 : if (!gst_buffer_map(buffer, &bufferMap, GST_MAP_READ))
350 : {
351 0 : GST_ERROR("Could not map audio buffer, discarding buffer!");
352 0 : writeFailure = true;
353 : }
354 : else
355 : {
356 4 : if (!m_clientBackend->writeBuffer(framesToWrite, bufferMap.data))
357 : {
358 1 : GST_ERROR("Could not map audio buffer, discarding buffer!");
359 1 : writeFailure = true;
360 : }
361 4 : gst_buffer_unmap(buffer, &bufferMap);
362 : }
363 : }
364 :
365 6 : if ((!writeFailure) && (framesToWrite * m_frameSize < bufferSize))
366 : {
367 : // Handle any leftover data
368 3 : uint32_t leftoverData = bufferSize - (availableFrames * m_frameSize);
369 3 : gst_buffer_resize(buffer, framesToWrite * m_frameSize, leftoverData);
370 3 : if ((leftoverData / m_frameSize == 0) && (m_dataBuffers.size() > 1))
371 : {
372 : // If the leftover data is smaller than a frame, it must be processed with the next buffer
373 1 : std::unique_lock lock{m_queueSizeMutex};
374 1 : m_dataBuffers.pop();
375 1 : m_dataBuffers.front() = gst_buffer_append(buffer, m_dataBuffers.front());
376 1 : gst_buffer_unref(buffer);
377 1 : m_queueSizeCv.notify_one();
378 : }
379 3 : }
380 : else
381 : {
382 3 : std::unique_lock lock{m_queueSizeMutex};
383 3 : m_dataBuffers.pop();
384 3 : gst_buffer_unref(buffer);
385 3 : m_queueSizeCv.notify_one();
386 : }
387 : }
388 15 : } while (!m_dataBuffers.empty() && availableFrames != 0);
389 :
390 : // If we still have samples stored that could not be pushed
391 : // This avoids any stoppages in the pushing of samples to the server if the consumption of
392 : // samples is slow.
393 12 : if (m_dataBuffers.size())
394 : {
395 : m_pushSamplesTimer =
396 6 : m_timerFactory->createTimer(std::chrono::milliseconds(10), [this]() { notifyPushSamplesTimerExpired(); });
397 : }
398 7 : else if (m_isEos)
399 : {
400 1 : m_clientBackend->setEos();
401 : }
402 : }
403 :
404 4 : bool GStreamerWebAudioPlayerClient::isNewConfig(const std::string &audioMimeType,
405 : std::weak_ptr<const firebolt::rialto::WebAudioConfig> webAudioConfig)
406 : {
407 4 : if (audioMimeType != m_mimeType)
408 : {
409 1 : return true;
410 : }
411 :
412 3 : if (audioMimeType != "audio/x-raw")
413 : {
414 1 : GST_ERROR("Cannot compare none pcm config");
415 1 : return true;
416 : }
417 :
418 2 : std::shared_ptr<const firebolt::rialto::WebAudioConfig> config = webAudioConfig.lock();
419 2 : if (!config || config->pcm != m_config.pcm)
420 : {
421 1 : return true;
422 : }
423 :
424 1 : return false;
425 2 : }
426 :
427 15 : void GStreamerWebAudioPlayerClient::notifyState(firebolt::rialto::WebAudioPlayerState state)
428 : {
429 15 : switch (state)
430 : {
431 2 : case firebolt::rialto::WebAudioPlayerState::END_OF_STREAM:
432 : {
433 2 : GST_INFO("Notify end of stream.");
434 2 : m_delegate.handleEos();
435 2 : m_isEos = false;
436 2 : break;
437 : }
438 2 : case firebolt::rialto::WebAudioPlayerState::FAILURE:
439 : {
440 2 : std::string errMessage = "Rialto server webaudio playback failed";
441 2 : GST_ERROR("%s", errMessage.c_str());
442 2 : m_delegate.handleError(errMessage);
443 2 : break;
444 : }
445 1 : case firebolt::rialto::WebAudioPlayerState::IDLE:
446 : {
447 1 : m_delegate.handleStateChanged(firebolt::rialto::PlaybackState::IDLE);
448 1 : break;
449 : }
450 8 : case firebolt::rialto::WebAudioPlayerState::PLAYING:
451 : {
452 8 : m_delegate.handleStateChanged(firebolt::rialto::PlaybackState::PLAYING);
453 8 : break;
454 : }
455 1 : case firebolt::rialto::WebAudioPlayerState::PAUSED:
456 : {
457 1 : m_delegate.handleStateChanged(firebolt::rialto::PlaybackState::PAUSED);
458 1 : break;
459 : }
460 1 : case firebolt::rialto::WebAudioPlayerState::UNKNOWN:
461 : default:
462 : {
463 1 : GST_WARNING("Web audio player sent unknown state");
464 1 : break;
465 : }
466 : }
467 15 : }
468 :
469 8 : bool GStreamerWebAudioPlayerClient::setVolume(double volume)
470 : {
471 8 : bool status{false};
472 16 : m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->setVolume(volume); });
473 8 : return status;
474 : }
475 :
476 6 : bool GStreamerWebAudioPlayerClient::getVolume(double &volume)
477 : {
478 6 : bool status{false};
479 12 : m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->getVolume(volume); });
480 6 : return status;
481 : }
|