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 51 : bool parseGstStructureFormat(const std::string &format, uint32_t &sampleSize, bool &isBigEndian, bool &isSigned,
35 : bool &isFloat)
36 : {
37 51 : if (format.size() != 5)
38 : {
39 1 : return false;
40 : }
41 50 : std::string sampleSizeStr = format.substr(1, 2);
42 50 : char *pEnd = NULL;
43 50 : errno = 0;
44 50 : sampleSize = strtoul(sampleSizeStr.c_str(), &pEnd, 10);
45 50 : if (errno == ERANGE)
46 : {
47 0 : return false;
48 : }
49 :
50 50 : isBigEndian = format.substr(3) == "BE";
51 :
52 50 : switch (format[0])
53 : {
54 45 : case 'S':
55 45 : isSigned = true;
56 45 : isFloat = false;
57 45 : 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 49 : return true;
71 50 : }
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 67 : GStreamerWebAudioPlayerClient::GStreamerWebAudioPlayerClient(
81 : std::unique_ptr<firebolt::rialto::client::WebAudioClientBackendInterface> &&webAudioClientBackend,
82 : std::unique_ptr<IMessageQueue> &&backendQueue, WebAudioSinkCallbacks callbacks,
83 67 : std::shared_ptr<ITimerFactory> timerFactory)
84 67 : : m_backendQueue{std::move(backendQueue)}, m_clientBackend{std::move(webAudioClientBackend)}, m_isOpen{false},
85 67 : m_dataBuffers{}, m_timerFactory{timerFactory}, m_pushSamplesTimer{nullptr}, m_preferredFrames{0},
86 67 : m_maximumFrames{0}, m_supportDeferredPlay{false}, m_isEos{false}, m_frameSize{0}, m_mimeType{}, m_config{{}},
87 201 : m_callbacks{callbacks}
88 : {
89 67 : m_backendQueue->start();
90 : }
91 :
92 67 : GStreamerWebAudioPlayerClient::~GStreamerWebAudioPlayerClient()
93 : {
94 67 : m_backendQueue->stop();
95 : }
96 :
97 56 : bool GStreamerWebAudioPlayerClient::open(GstCaps *caps)
98 : {
99 56 : GST_DEBUG("entry:");
100 :
101 56 : bool result = false;
102 56 : GstStructure *structure = gst_caps_get_structure(caps, 0);
103 56 : std::string audioMimeType = gst_structure_get_name(structure);
104 56 : const auto spacePosition = audioMimeType.find(' ');
105 56 : if (spacePosition != std::string::npos)
106 : {
107 0 : audioMimeType.resize(spacePosition);
108 : }
109 56 : const gchar *formatCStr{gst_structure_get_string(structure, "format")};
110 112 : std::string format{formatCStr ? formatCStr : ""};
111 : firebolt::rialto::WebAudioPcmConfig pcm;
112 : gint tmp;
113 :
114 56 : if (format.empty())
115 : {
116 3 : GST_ERROR("Format not found in caps");
117 3 : return result;
118 : }
119 :
120 53 : if (!gst_structure_get_int(structure, "rate", &tmp))
121 : {
122 1 : GST_ERROR("Rate not found in caps");
123 1 : return result;
124 : }
125 52 : pcm.rate = tmp;
126 :
127 52 : if (!gst_structure_get_int(structure, "channels", &tmp))
128 : {
129 1 : GST_ERROR("Rate not found in caps");
130 1 : return result;
131 : }
132 51 : pcm.channels = tmp;
133 :
134 51 : 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 98 : m_backendQueue->callInEventLoop(
141 49 : [&]()
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 49 : firebolt::rialto::WebAudioConfig configWorkaround{pcm};
147 : std::shared_ptr<firebolt::rialto::WebAudioConfig> config =
148 49 : std::make_shared<firebolt::rialto::WebAudioConfig>(configWorkaround);
149 :
150 : // Only recreate player if the config has changed
151 49 : if (!m_isOpen || isNewConfig(audioMimeType, config))
152 : {
153 48 : if (m_isOpen)
154 : {
155 : // Destroy the previously created player
156 3 : m_clientBackend->destroyWebAudioBackend();
157 : }
158 :
159 48 : uint32_t priority = 1;
160 48 : if (m_clientBackend->createWebAudioBackend(shared_from_this(), audioMimeType, priority, config))
161 : {
162 47 : if (!m_clientBackend->getDeviceInfo(m_preferredFrames, m_maximumFrames, m_supportDeferredPlay))
163 : {
164 1 : GST_ERROR("GetDeviceInfo failed, could not process samples");
165 : }
166 47 : m_frameSize = (pcm.sampleSize * pcm.channels) / CHAR_BIT;
167 47 : m_isOpen = true;
168 :
169 : // Store config
170 47 : m_config.pcm = pcm;
171 47 : m_mimeType = audioMimeType;
172 : }
173 : else
174 : {
175 1 : GST_ERROR("Could not create web audio backend");
176 1 : m_isOpen = false;
177 : }
178 48 : result = m_isOpen;
179 : }
180 49 : });
181 :
182 49 : return result;
183 56 : }
184 :
185 20 : bool GStreamerWebAudioPlayerClient::close()
186 : {
187 20 : GST_DEBUG("entry:");
188 :
189 40 : m_backendQueue->callInEventLoop(
190 20 : [&]()
191 : {
192 20 : m_clientBackend->destroyWebAudioBackend();
193 20 : m_pushSamplesTimer.reset();
194 20 : m_isOpen = false;
195 20 : });
196 :
197 20 : return true;
198 : }
199 :
200 12 : bool GStreamerWebAudioPlayerClient::play()
201 : {
202 12 : GST_DEBUG("entry:");
203 :
204 12 : bool result = false;
205 24 : m_backendQueue->callInEventLoop(
206 12 : [&]()
207 : {
208 12 : if (m_isOpen)
209 : {
210 11 : result = m_clientBackend->play();
211 : }
212 : else
213 : {
214 1 : GST_ERROR("No web audio backend");
215 : }
216 12 : });
217 :
218 12 : return result;
219 : }
220 :
221 12 : bool GStreamerWebAudioPlayerClient::pause()
222 : {
223 12 : GST_DEBUG("entry:");
224 :
225 12 : bool result = false;
226 24 : m_backendQueue->callInEventLoop(
227 12 : [&]()
228 : {
229 12 : if (m_isOpen)
230 : {
231 11 : result = m_clientBackend->pause();
232 : }
233 : else
234 : {
235 1 : GST_ERROR("No web audio backend");
236 : }
237 12 : });
238 :
239 12 : return result;
240 : }
241 :
242 7 : bool GStreamerWebAudioPlayerClient::setEos()
243 : {
244 7 : GST_DEBUG("entry:");
245 :
246 7 : bool result = false;
247 14 : m_backendQueue->callInEventLoop(
248 7 : [&]()
249 : {
250 7 : if (m_isOpen && !m_isEos)
251 : {
252 5 : m_isEos = true;
253 5 : if (m_dataBuffers.empty())
254 : {
255 4 : 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 7 : });
268 :
269 7 : return result;
270 : }
271 :
272 21 : bool GStreamerWebAudioPlayerClient::isOpen()
273 : {
274 21 : GST_DEBUG("entry:");
275 :
276 21 : bool result = false;
277 42 : m_backendQueue->callInEventLoop([&]() { result = m_isOpen; });
278 :
279 21 : return result;
280 : }
281 :
282 2 : void GStreamerWebAudioPlayerClient::notifyPushSamplesTimerExpired()
283 : {
284 4 : m_backendQueue->scheduleInEventLoop([&]() { pushSamples(); });
285 2 : }
286 :
287 9 : bool GStreamerWebAudioPlayerClient::notifyNewSample(GstBuffer *buf)
288 : {
289 9 : GST_DEBUG("entry:");
290 :
291 9 : bool result = false;
292 :
293 : {
294 9 : std::unique_lock lock{m_queueSizeMutex};
295 18 : m_queueSizeCv.wait(lock, [&]() { return m_dataBuffers.size() < kMaxQueueSize; });
296 9 : }
297 :
298 18 : m_backendQueue->callInEventLoop(
299 9 : [&]()
300 : {
301 9 : if (buf)
302 : {
303 9 : if (m_pushSamplesTimer)
304 : {
305 1 : m_pushSamplesTimer->cancel();
306 1 : m_pushSamplesTimer.reset();
307 : }
308 : {
309 9 : std::unique_lock lock{m_queueSizeMutex};
310 9 : m_dataBuffers.push(buf);
311 : }
312 9 : pushSamples();
313 9 : result = true;
314 : }
315 9 : });
316 :
317 9 : return result;
318 : }
319 :
320 12 : void GStreamerWebAudioPlayerClient::pushSamples()
321 : {
322 12 : GST_DEBUG("entry:");
323 12 : if (!m_isOpen || m_dataBuffers.empty())
324 : {
325 1 : return;
326 : }
327 :
328 11 : uint32_t availableFrames = 0u;
329 : do
330 : {
331 14 : 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 10 : else if (0 != availableFrames)
341 : {
342 5 : bool writeFailure = false;
343 5 : GstBuffer *buffer = m_dataBuffers.front();
344 5 : gsize bufferSize = gst_buffer_get_size(buffer);
345 5 : auto framesToWrite = std::min(availableFrames, static_cast<uint32_t>(bufferSize / m_frameSize));
346 5 : 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 5 : 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 2 : std::unique_lock lock{m_queueSizeMutex};
383 2 : m_dataBuffers.pop();
384 2 : gst_buffer_unref(buffer);
385 2 : m_queueSizeCv.notify_one();
386 : }
387 : }
388 14 : } 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 11 : if (m_dataBuffers.size())
394 : {
395 : m_pushSamplesTimer =
396 6 : m_timerFactory->createTimer(std::chrono::milliseconds(10), [this]() { notifyPushSamplesTimerExpired(); });
397 : }
398 6 : 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 16 : void GStreamerWebAudioPlayerClient::notifyState(firebolt::rialto::WebAudioPlayerState state)
428 : {
429 16 : switch (state)
430 : {
431 3 : case firebolt::rialto::WebAudioPlayerState::END_OF_STREAM:
432 : {
433 3 : GST_INFO("Notify end of stream.");
434 3 : if (m_callbacks.eosCallback)
435 : {
436 3 : m_callbacks.eosCallback();
437 : }
438 3 : m_isEos = false;
439 3 : break;
440 : }
441 2 : case firebolt::rialto::WebAudioPlayerState::FAILURE:
442 : {
443 2 : std::string errMessage = "Rialto server webaudio playback failed";
444 2 : GST_ERROR("%s", errMessage.c_str());
445 2 : if (m_callbacks.errorCallback)
446 : {
447 2 : m_callbacks.errorCallback(errMessage.c_str());
448 : }
449 2 : break;
450 : }
451 10 : case firebolt::rialto::WebAudioPlayerState::IDLE:
452 : case firebolt::rialto::WebAudioPlayerState::PLAYING:
453 : case firebolt::rialto::WebAudioPlayerState::PAUSED:
454 : {
455 10 : if (m_callbacks.stateChangedCallback)
456 : {
457 10 : m_callbacks.stateChangedCallback(state);
458 : }
459 10 : break;
460 : }
461 1 : case firebolt::rialto::WebAudioPlayerState::UNKNOWN:
462 : default:
463 : {
464 1 : GST_WARNING("Web audio player sent unknown state");
465 1 : break;
466 : }
467 : }
468 16 : }
469 :
470 4 : bool GStreamerWebAudioPlayerClient::setVolume(double volume)
471 : {
472 4 : bool status{false};
473 8 : m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->setVolume(volume); });
474 4 : return status;
475 : }
476 :
477 3 : bool GStreamerWebAudioPlayerClient::getVolume(double &volume)
478 : {
479 3 : bool status{false};
480 6 : m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->getVolume(volume); });
481 3 : return status;
482 : }
|