1 | // Copyright (C) 2016 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qsamplecache_p.h" |
5 | #include "qwavedecoder.h" |
6 | #include "qfile.h" |
7 | #include "qmultimediautils_p.h" |
8 | |
9 | #if QT_CONFIG(network) |
10 | # include <QtNetwork/QNetworkAccessManager> |
11 | # include <QtNetwork/QNetworkReply> |
12 | # include <QtNetwork/QNetworkRequest> |
13 | #endif |
14 | |
15 | #include <QtCore/QDebug> |
16 | #include <QtCore/qloggingcategory.h> |
17 | |
18 | static Q_LOGGING_CATEGORY(qLcSampleCache, "qt.multimedia.samplecache") |
19 | |
20 | #include <mutex> |
21 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | |
25 | /*! |
26 | \class QSampleCache |
27 | \internal |
28 | |
29 | When you want to get a sound sample data, you need to request the QSample reference from QSampleCache. |
30 | |
31 | |
32 | \code |
33 | QSample *m_sample; // class member. |
34 | |
35 | private Q_SLOTS: |
36 | void decoderError(); |
37 | void sampleReady(); |
38 | \endcode |
39 | |
40 | \code |
41 | Q_GLOBAL_STATIC(QSampleCache, sampleCache) //declare a singleton manager |
42 | \endcode |
43 | |
44 | \code |
45 | m_sample = sampleCache()->requestSample(url); |
46 | switch(m_sample->state()) { |
47 | case QSample::Ready: |
48 | sampleReady(); |
49 | break; |
50 | case QSample::Error: |
51 | decoderError(); |
52 | break; |
53 | default: |
54 | connect(m_sample, SIGNAL(error()), this, SLOT(decoderError())); |
55 | connect(m_sample, SIGNAL(ready()), this, SLOT(sampleReady())); |
56 | break; |
57 | } |
58 | \endcode |
59 | |
60 | When you no longer need the sound sample data, you need to release it: |
61 | |
62 | \code |
63 | if (m_sample) { |
64 | m_sample->release(); |
65 | m_sample = 0; |
66 | } |
67 | \endcode |
68 | */ |
69 | |
70 | QSampleCache::QSampleCache(QObject *parent) |
71 | : QObject(parent), m_capacity(0), m_usage(0), m_loadingRefCount(0) |
72 | { |
73 | m_loadingThread.setObjectName(QLatin1String("QSampleCache::LoadingThread")); |
74 | } |
75 | |
76 | std::unique_ptr<QIODevice> QSampleCache::createStreamForSample(QSample &sample) |
77 | { |
78 | #if QT_CONFIG(network) |
79 | if (m_sampleSourceType == SampleSourceType::NetworkManager) { |
80 | if (sample.m_url.scheme().isEmpty()) { |
81 | // exit early, to avoid QNetworkAccessManager trying to construct a default ssl |
82 | // configuration, which tends to cause timeouts on CI on macos. |
83 | // catch this case and exit early. |
84 | return nullptr; |
85 | } |
86 | |
87 | if (!m_networkAccessManager) |
88 | m_networkAccessManager = std::make_unique<QNetworkAccessManager>(); |
89 | std::unique_ptr<QNetworkReply> reply( |
90 | m_networkAccessManager->get(request: QNetworkRequest(sample.m_url))); |
91 | if (reply) |
92 | connect(sender: reply.get(), signal: &QNetworkReply::errorOccurred, context: &sample, |
93 | slot: &QSample::handleLoadingError); |
94 | return reply; |
95 | } |
96 | #endif |
97 | |
98 | // The QFile source is needed of the QtLite build, which excludes networking |
99 | if (m_sampleSourceType == SampleSourceType::File) { |
100 | auto file = std::make_unique<QFile>(args: sample.m_url.toLocalFile()); |
101 | if (file->open(flags: QFile::ReadOnly)) |
102 | return file; |
103 | } |
104 | |
105 | return nullptr; |
106 | } |
107 | |
108 | QSampleCache::~QSampleCache() |
109 | { |
110 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
111 | |
112 | m_loadingThread.quit(); |
113 | m_loadingThread.wait(); |
114 | |
115 | // Killing the loading thread means that no samples can be |
116 | // deleted using deleteLater. And some samples that had deleteLater |
117 | // already called won't have been processed (m_staleSamples) |
118 | for (auto it = m_samples.cbegin(), end = m_samples.cend(); it != end; ++it) |
119 | delete it.value(); |
120 | |
121 | const auto copyStaleSamples = m_staleSamples; //deleting a sample does affect the m_staleSamples list, but we create a copy |
122 | for (QSample* sample : copyStaleSamples) |
123 | delete sample; |
124 | |
125 | #if QT_CONFIG(network) |
126 | // Should we delete it under the mutex? |
127 | m_networkAccessManager.reset(); |
128 | #endif |
129 | } |
130 | |
131 | void QSampleCache::loadingRelease() |
132 | { |
133 | QMutexLocker locker(&m_loadingMutex); |
134 | m_loadingRefCount--; |
135 | if (m_loadingRefCount == 0) { |
136 | if (m_loadingThread.isRunning()) { |
137 | #if QT_CONFIG(network) |
138 | if (m_networkAccessManager) |
139 | m_networkAccessManager.release()->deleteLater(); |
140 | #endif |
141 | m_loadingThread.exit(); |
142 | } |
143 | } |
144 | } |
145 | |
146 | bool QSampleCache::isLoading() const |
147 | { |
148 | return m_loadingThread.isRunning(); |
149 | } |
150 | |
151 | bool QSampleCache::isCached(const QUrl &url) const |
152 | { |
153 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
154 | return m_samples.contains(key: url); |
155 | } |
156 | |
157 | QSample* QSampleCache::requestSample(const QUrl& url) |
158 | { |
159 | //lock and add first to make sure live loadingThread will not be killed during this function call |
160 | m_loadingMutex.lock(); |
161 | const bool needsThreadStart = m_loadingRefCount == 0; |
162 | m_loadingRefCount++; |
163 | m_loadingMutex.unlock(); |
164 | |
165 | qCDebug(qLcSampleCache) << "QSampleCache: request sample ["<< url << "]"; |
166 | std::unique_lock<QRecursiveMutex> locker(m_mutex); |
167 | QMap<QUrl, QSample*>::iterator it = m_samples.find(key: url); |
168 | QSample* sample; |
169 | if (it == m_samples.end()) { |
170 | if (needsThreadStart) { |
171 | // Previous thread might be finishing, need to wait for it. If not, this is a no-op. |
172 | m_loadingThread.wait(); |
173 | m_loadingThread.start(); |
174 | } |
175 | sample = new QSample(url, this); |
176 | m_samples.insert(key: url, value: sample); |
177 | #if QT_CONFIG(thread) |
178 | sample->moveToThread(thread: &m_loadingThread); |
179 | #endif |
180 | } else { |
181 | sample = *it; |
182 | if (sample->state() == QSample::Error && needsThreadStart) { |
183 | m_loadingThread.wait(); |
184 | m_loadingThread.start(); |
185 | } |
186 | } |
187 | |
188 | sample->addRef(); |
189 | locker.unlock(); |
190 | |
191 | sample->loadIfNecessary(); |
192 | return sample; |
193 | } |
194 | |
195 | void QSampleCache::setCapacity(qint64 capacity) |
196 | { |
197 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
198 | if (m_capacity == capacity) |
199 | return; |
200 | qCDebug(qLcSampleCache) << "QSampleCache: capacity changes from "<< m_capacity << "to "<< capacity; |
201 | if (m_capacity > 0 && capacity <= 0) { //memory management strategy changed |
202 | for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) { |
203 | QSample* sample = *it; |
204 | if (sample->m_ref == 0) { |
205 | unloadSample(sample); |
206 | it = m_samples.erase(it); |
207 | } else { |
208 | ++it; |
209 | } |
210 | } |
211 | } |
212 | |
213 | m_capacity = capacity; |
214 | refresh(usageChange: 0); |
215 | } |
216 | |
217 | // Called locked |
218 | void QSampleCache::unloadSample(QSample *sample) |
219 | { |
220 | m_usage -= sample->m_soundData.size(); |
221 | m_staleSamples.insert(value: sample); |
222 | sample->deleteLater(); |
223 | } |
224 | |
225 | // Called in both threads |
226 | void QSampleCache::refresh(qint64 usageChange) |
227 | { |
228 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
229 | m_usage += usageChange; |
230 | if (m_capacity <= 0 || m_usage <= m_capacity) |
231 | return; |
232 | |
233 | qint64 recoveredSize = 0; |
234 | |
235 | //free unused samples to keep usage under capacity limit. |
236 | for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) { |
237 | QSample* sample = *it; |
238 | if (sample->m_ref > 0) { |
239 | ++it; |
240 | continue; |
241 | } |
242 | recoveredSize += sample->m_soundData.size(); |
243 | unloadSample(sample); |
244 | it = m_samples.erase(it); |
245 | if (m_usage <= m_capacity) |
246 | return; |
247 | } |
248 | |
249 | qCDebug(qLcSampleCache) << "QSampleCache: refresh("<< usageChange |
250 | << ") recovered size ="<< recoveredSize |
251 | << "new usage ="<< m_usage; |
252 | |
253 | if (m_usage > m_capacity) |
254 | qWarning() << "QSampleCache: usage"<< m_usage << "out of limit"<< m_capacity; |
255 | } |
256 | |
257 | // Called in both threads |
258 | void QSampleCache::removeUnreferencedSample(QSample *sample) |
259 | { |
260 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
261 | m_staleSamples.remove(value: sample); |
262 | } |
263 | |
264 | // Called in loader thread (since this lives in that thread) |
265 | // Also called from application thread after loader thread dies. |
266 | QSample::~QSample() |
267 | { |
268 | // Remove ourselves from our parent |
269 | m_parent->removeUnreferencedSample(sample: this); |
270 | |
271 | QMutexLocker locker(&m_mutex); |
272 | qCDebug(qLcSampleCache) << "~QSample"<< this << ": deleted ["<< m_url << "]"<< QThread::currentThread(); |
273 | cleanup(); |
274 | } |
275 | |
276 | // Called in application thread |
277 | void QSample::loadIfNecessary() |
278 | { |
279 | QMutexLocker locker(&m_mutex); |
280 | if (m_state == QSample::Error || m_state == QSample::Creating) { |
281 | m_state = QSample::Loading; |
282 | QMetaObject::invokeMethod(object: this, function: &QSample::load, type: Qt::QueuedConnection); |
283 | } else { |
284 | m_parent->loadingRelease(); |
285 | } |
286 | } |
287 | |
288 | // Called in application thread |
289 | bool QSampleCache::notifyUnreferencedSample(QSample* sample) |
290 | { |
291 | if (m_loadingThread.isRunning()) |
292 | m_loadingThread.wait(); |
293 | |
294 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
295 | |
296 | if (m_capacity > 0) |
297 | return false; |
298 | m_samples.remove(key: sample->m_url); |
299 | unloadSample(sample); |
300 | return true; |
301 | } |
302 | |
303 | // Called in application thread |
304 | void QSample::release() |
305 | { |
306 | QMutexLocker locker(&m_mutex); |
307 | qCDebug(qLcSampleCache) << "Sample:: release"<< this << QThread::currentThread() << m_ref; |
308 | if (--m_ref == 0) { |
309 | locker.unlock(); |
310 | m_parent->notifyUnreferencedSample(sample: this); |
311 | } |
312 | } |
313 | |
314 | // Called in dtor and when stream is loaded |
315 | // must be called locked. |
316 | void QSample::cleanup() |
317 | { |
318 | qCDebug(qLcSampleCache) << "QSample: cleanup"; |
319 | if (m_waveDecoder) { |
320 | m_waveDecoder->disconnect(receiver: this); |
321 | m_waveDecoder.release()->deleteLater(); |
322 | } |
323 | |
324 | if (m_stream) { |
325 | m_stream->disconnect(receiver: this); |
326 | m_stream.release()->deleteLater(); |
327 | } |
328 | } |
329 | |
330 | // Called in application thread |
331 | void QSample::addRef() |
332 | { |
333 | m_ref++; |
334 | } |
335 | |
336 | // Called in loading thread |
337 | void QSample::readSample() |
338 | { |
339 | #if QT_CONFIG(thread) |
340 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
341 | #endif |
342 | QMutexLocker m(&m_mutex); |
343 | qint64 read = m_waveDecoder->read(data: m_soundData.data() + m_sampleReadLength, |
344 | maxlen: qMin(a: m_waveDecoder->bytesAvailable(), |
345 | b: qint64(m_waveDecoder->size() - m_sampleReadLength))); |
346 | qCDebug(qLcSampleCache) << "QSample: readSample"<< read; |
347 | if (read > 0) |
348 | m_sampleReadLength += read; |
349 | if (m_sampleReadLength < m_waveDecoder->size()) |
350 | return; |
351 | Q_ASSERT(m_sampleReadLength == qint64(m_soundData.size())); |
352 | onReady(); |
353 | } |
354 | |
355 | // Called in loading thread |
356 | void QSample::decoderReady() |
357 | { |
358 | #if QT_CONFIG(thread) |
359 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
360 | #endif |
361 | QMutexLocker m(&m_mutex); |
362 | qCDebug(qLcSampleCache) << "QSample: decoder ready"; |
363 | m_parent->refresh(usageChange: m_waveDecoder->size()); |
364 | |
365 | m_soundData.resize(size: m_waveDecoder->size()); |
366 | m_sampleReadLength = 0; |
367 | qint64 read = m_waveDecoder->read(data: m_soundData.data(), maxlen: m_waveDecoder->size()); |
368 | qCDebug(qLcSampleCache) << " bytes read"<< read; |
369 | if (read > 0) |
370 | m_sampleReadLength += read; |
371 | if (m_sampleReadLength >= m_waveDecoder->size()) |
372 | onReady(); |
373 | } |
374 | |
375 | // Called in all threads |
376 | QSample::State QSample::state() const |
377 | { |
378 | QMutexLocker m(&m_mutex); |
379 | return m_state; |
380 | } |
381 | |
382 | // Called in loading thread |
383 | // Essentially a second ctor, doesn't need locks (?) |
384 | void QSample::load() |
385 | { |
386 | #if QT_CONFIG(thread) |
387 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
388 | #endif |
389 | qCDebug(qLcSampleCache) << "QSample: load ["<< m_url << "]"; |
390 | |
391 | m_stream = m_parent->createStreamForSample(sample&: *this); |
392 | |
393 | if (!m_stream) { |
394 | handleLoadingError(); |
395 | return; |
396 | } |
397 | |
398 | m_waveDecoder = std::make_unique<QWaveDecoder>(args: m_stream.get()); |
399 | connect(sender: m_waveDecoder.get(), signal: &QWaveDecoder::formatKnown, context: this, slot: &QSample::decoderReady); |
400 | connect(sender: m_waveDecoder.get(), signal: &QWaveDecoder::parsingError, context: this, slot: &QSample::decoderError); |
401 | connect(sender: m_waveDecoder.get(), signal: &QIODevice::readyRead, context: this, slot: &QSample::readSample); |
402 | |
403 | m_waveDecoder->open(mode: QIODevice::ReadOnly); |
404 | } |
405 | |
406 | void QSample::handleLoadingError(int errorCode) |
407 | { |
408 | #if QT_CONFIG(thread) |
409 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
410 | #endif |
411 | QMutexLocker m(&m_mutex); |
412 | qCDebug(qLcSampleCache) << "QSample: loading error:"<< errorCode |
413 | << "source type: "<< qToUnderlying(e: m_parent->sampleSourceType()); |
414 | cleanup(); |
415 | m_state = QSample::Error; |
416 | m_parent->loadingRelease(); |
417 | emit error(self: this); |
418 | } |
419 | |
420 | // Called in loading thread |
421 | void QSample::decoderError() |
422 | { |
423 | #if QT_CONFIG(thread) |
424 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
425 | #endif |
426 | QMutexLocker m(&m_mutex); |
427 | qCDebug(qLcSampleCache) << "QSample: decoder error"; |
428 | cleanup(); |
429 | m_state = QSample::Error; |
430 | m_parent->loadingRelease(); |
431 | emit error(self: this); |
432 | } |
433 | |
434 | // Called in loading thread from decoder when sample is done. Locked already. |
435 | void QSample::onReady() |
436 | { |
437 | #if QT_CONFIG(thread) |
438 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread")); |
439 | #endif |
440 | m_audioFormat = m_waveDecoder->audioFormat(); |
441 | qCDebug(qLcSampleCache) << "QSample: load ready format:"<< m_audioFormat; |
442 | cleanup(); |
443 | m_state = QSample::Ready; |
444 | m_parent->loadingRelease(); |
445 | emit ready(self: this); |
446 | } |
447 | |
448 | // Called in application thread, then moved to loader thread |
449 | QSample::QSample(const QUrl &url, QSampleCache *parent) |
450 | : m_parent(parent), |
451 | m_waveDecoder(nullptr), |
452 | m_url(url), |
453 | m_sampleReadLength(0), |
454 | m_state(Creating), |
455 | m_ref(0) |
456 | { |
457 | } |
458 | |
459 | QT_END_NAMESPACE |
460 | |
461 | #include "moc_qsamplecache_p.cpp" |
462 |
Definitions
Learn Advanced QML with KDAB
Find out more