File indexing completed on 2024-04-28 11:43:47

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2014-2015 Martin Klapetek <mklapetek@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "notifybyaudio_phonon.h"
0009 #include "debug_p.h"
0010 
0011 #include <QFile>
0012 #include <QString>
0013 #include <QUrl>
0014 
0015 #include "knotification.h"
0016 #include "knotifyconfig.h"
0017 
0018 #include <phonon/audiooutput.h>
0019 #include <phonon/mediaobject.h>
0020 #include <phonon/mediasource.h>
0021 
0022 NotifyByAudio::NotifyByAudio(QObject *parent)
0023     : KNotificationPlugin(parent)
0024     , m_audioOutput(nullptr)
0025 {
0026 }
0027 
0028 NotifyByAudio::~NotifyByAudio()
0029 {
0030     qDeleteAll(m_reusablePhonons);
0031     delete m_audioOutput;
0032 }
0033 
0034 void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config)
0035 {
0036     if (!m_audioOutput) {
0037         m_audioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, this);
0038     }
0039     const QString soundFilename = config->readEntry(QStringLiteral("Sound"));
0040     if (soundFilename.isEmpty()) {
0041         qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification";
0042 
0043         finish(notification);
0044         return;
0045     }
0046 
0047     QUrl soundURL;
0048     const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0049     for (const QString &dataLocation : dataLocations) {
0050         soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile);
0051         if (soundURL.isLocalFile() && QFile::exists(soundURL.toLocalFile())) {
0052             break;
0053         } else if (!soundURL.isLocalFile() && soundURL.isValid()) {
0054             break;
0055         }
0056         soundURL.clear();
0057     }
0058     if (soundURL.isEmpty()) {
0059         qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification";
0060         finish(notification);
0061         return;
0062     }
0063 
0064     Phonon::MediaObject *m;
0065 
0066     if (m_reusablePhonons.isEmpty()) {
0067         m = new Phonon::MediaObject(this);
0068         connect(m, &Phonon::MediaObject::finished, this, &NotifyByAudio::onAudioFinished);
0069         connect(m, &Phonon::MediaObject::stateChanged, this, &NotifyByAudio::stateChanged);
0070         Phonon::createPath(m, m_audioOutput);
0071     } else {
0072         m = m_reusablePhonons.takeFirst();
0073     }
0074 
0075     m->setCurrentSource(soundURL);
0076     m->play();
0077 
0078     if (notification->flags() & KNotification::LoopSound) {
0079         // Enqueuing essentially prevents the subsystem pipeline from partial teardown
0080         // which is the most desired thing in terms of load and delay between loop cycles.
0081         // All of this is timing dependent, which is why we want at least one source queued;
0082         // in reality the shorter the source the more sources we want to be queued to prevent
0083         // the MO from running out of sources.
0084         // Point being that all phonon signals are forcefully queued (because qthread has problems detecting !pthread threads),
0085         // so when you get for example the aboutToFinish signal the MO might already have stopped playing.
0086         //
0087         // And so we queue it three times at least; doesn't cost anything and keeps us safe.
0088 
0089         m->enqueue(soundURL);
0090         m->enqueue(soundURL);
0091         m->enqueue(soundURL);
0092 
0093         connect(m, &Phonon::MediaObject::currentSourceChanged, this, &NotifyByAudio::onAudioSourceChanged);
0094     }
0095 
0096     Q_ASSERT(!m_notifications.value(m));
0097     m_notifications.insert(m, notification);
0098 }
0099 
0100 void NotifyByAudio::stateChanged(Phonon::State newState, Phonon::State oldState)
0101 {
0102     qCDebug(LOG_KNOTIFICATIONS) << "Changing audio state from" << oldState << "to" << newState;
0103 }
0104 
0105 void NotifyByAudio::close(KNotification *notification)
0106 {
0107     Phonon::MediaObject *m = m_notifications.key(notification);
0108 
0109     if (!m) {
0110         return;
0111     }
0112 
0113     m->stop();
0114     finishNotification(notification, m);
0115 }
0116 
0117 void NotifyByAudio::onAudioFinished()
0118 {
0119     Phonon::MediaObject *m = qobject_cast<Phonon::MediaObject *>(sender());
0120 
0121     if (!m) {
0122         return;
0123     }
0124 
0125     KNotification *notification = m_notifications.value(m, nullptr);
0126 
0127     if (!notification) {
0128         // This means that close was called already so there's nothing else to do.
0129         // Ideally we should not be getting here if close has already been called
0130         // since stopping a mediaobject means it won't emit finished() *BUT*
0131         // since the finished signal is a queued connection in phonon it can happen
0132         // that the playing had already finished and we just had not got the signal yet
0133         return;
0134     }
0135 
0136     // if the sound is short enough, we can't guarantee new sounds are
0137     // enqueued before finished is emitted.
0138     // so to make sure we are looping restart it when the sound finished
0139     if (notification && (notification->flags() & KNotification::LoopSound)) {
0140         m->play();
0141         return;
0142     }
0143 
0144     finishNotification(notification, m);
0145 }
0146 
0147 void NotifyByAudio::finishNotification(KNotification *notification, Phonon::MediaObject *m)
0148 {
0149     m_notifications.remove(m);
0150 
0151     if (notification) {
0152         finish(notification);
0153     }
0154 
0155     disconnect(m, &Phonon::MediaObject::currentSourceChanged, this, &NotifyByAudio::onAudioSourceChanged);
0156 
0157     m_reusablePhonons.append(m);
0158 }
0159 
0160 void NotifyByAudio::onAudioSourceChanged(const Phonon::MediaSource &source)
0161 {
0162     Phonon::MediaObject *m = qobject_cast<Phonon::MediaObject *>(sender());
0163 
0164     if (!m) {
0165         return;
0166     }
0167 
0168     m->enqueue(source);
0169 }
0170 
0171 #include "moc_notifybyaudio_phonon.cpp"