File indexing completed on 2025-01-05 04:29:50

0001 /**
0002  * SPDX-FileCopyrightText: 2022-2023 Bart De Vries <bart@mogwai.be>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005  */
0006 
0007 #include "gstmediabackend.h"
0008 #include "gstmediabackendlogging.h"
0009 #include "gstsignalslogging.h"
0010 
0011 #include <QAudio>
0012 #include <QDebug>
0013 #include <QImage>
0014 #include <QString>
0015 #include <QTemporaryDir>
0016 #include <QTimer>
0017 
0018 class GstMediaBackendPrivate
0019 {
0020 private:
0021     friend class GstMediaBackend;
0022 
0023     const qint64 m_notifyInterval = 500; // interval for position updates (in ms)
0024 
0025     KMediaSession *m_kMediaSession = nullptr;
0026     GstElement *m_pipeline = nullptr;
0027     GstElement *m_playbin = nullptr;
0028     GstElement *m_audioBin = nullptr;
0029     GstElement *m_audioSink = nullptr;
0030     GstElement *m_videoBin = nullptr;
0031     GstElement *m_scaleTempo = nullptr;
0032     GstElement *m_audioConvert = nullptr;
0033     GstElement *m_audioResample = nullptr;
0034     GstBus *m_bus = nullptr;
0035     GstMessage *m_msg = nullptr;
0036     GstStateChangeReturn m_ret;
0037 
0038     QTimer *m_timer = nullptr;
0039     QUrl m_source;
0040     qint64 m_position = 0;
0041     qint64 m_duration = 0;
0042     qreal m_rate = 1.0;
0043     qreal m_volume = 100;
0044     bool m_muted = false;
0045     bool m_seekable = false;
0046     KMediaSession::MediaStatus m_mediaStatus = KMediaSession::MediaStatus::NoMedia;
0047     KMediaSession::Error m_error = KMediaSession::Error::NoError;
0048     KMediaSession::PlaybackState m_playbackState = KMediaSession::PlaybackState::StoppedState;
0049     std::unique_ptr<QTemporaryDir> m_imageCacheDir = nullptr;
0050 
0051     bool m_seekPending = false;
0052     qint64 m_positionBeforeSeek = 0;
0053     qint64 m_positionAfterSeek = 0;
0054 
0055     void parseMetaData(GstTagList *tags);
0056 
0057     void playerSeekableChanged(GstMediaBackend *backend);
0058     static void playerSignalVolumeChanged(GObject *o, GParamSpec *p, gpointer d);
0059     static void playerSignalMutedChanged(GObject *o, GParamSpec *p, gpointer d);
0060     static void playerSignalPlaybackRateChanged(GObject *o, GParamSpec *p, gpointer d);
0061 
0062     static gboolean busCallback(GstBus *, GstMessage *message, gpointer pointer)
0063     {
0064         gst_message_ref(message);
0065         reinterpret_cast<GstMediaBackend *>(pointer)->handleMessage(message);
0066         return TRUE;
0067     };
0068 };
0069 
0070 GstMediaBackend::GstMediaBackend(QObject *parent)
0071     : AbstractMediaBackend(parent)
0072     , d(std::make_unique<GstMediaBackendPrivate>())
0073 {
0074     qCDebug(GstMediaBackendLog) << "GstMediaBackend::GstMediaBackend()";
0075     d->m_kMediaSession = static_cast<KMediaSession *>(parent);
0076 
0077     // create timer to get position updates
0078     d->m_timer = new QTimer(this);
0079     connect(d->m_timer, &QTimer::timeout, this, &GstMediaBackend::timerUpdate);
0080 
0081     // gstreamer initialization
0082     gst_init(nullptr, nullptr);
0083 
0084     // building pipeline
0085     d->m_pipeline = gst_element_factory_make("playbin", "myplaybin");
0086     d->m_scaleTempo = gst_element_factory_make("scaletempo", "scale_tempo");
0087     d->m_audioConvert = gst_element_factory_make("audioconvert", "convert");
0088     d->m_audioSink = gst_element_factory_make("autoaudiosink", "audio_sink");
0089     if (!d->m_scaleTempo || !d->m_audioConvert || !d->m_audioSink) {
0090         qCDebug(GstMediaBackendLog) << "Not all elements could be created.";
0091     }
0092 
0093     /* Create the audio sink bin, add the elements and link them */
0094     d->m_audioBin = gst_bin_new("audio_sink_bin");
0095     gst_bin_add_many(GST_BIN(d->m_audioBin), d->m_scaleTempo, d->m_audioConvert, d->m_audioSink, NULL);
0096     gst_element_link_many(d->m_scaleTempo, d->m_audioConvert, d->m_audioSink, nullptr);
0097     GstPad *pad_audio = gst_element_get_static_pad(d->m_scaleTempo, "sink");
0098     GstPad *ghost_pad_audio = gst_ghost_pad_new("sink", pad_audio);
0099     gst_pad_set_active(ghost_pad_audio, TRUE);
0100     gst_element_add_pad(d->m_audioBin, ghost_pad_audio);
0101     gst_object_unref(pad_audio);
0102 
0103     /* Set playbin's audio sink to be our sink bin */
0104     g_object_set(GST_OBJECT(d->m_pipeline), "audio-sink", d->m_audioBin, nullptr);
0105 
0106     // get bus and connect to get messages sent through callbacks
0107     // TODO: implement fallback in case gmainloop support is not available in qt
0108     d->m_bus = gst_element_get_bus(d->m_pipeline);
0109     gst_bus_add_watch_full(d->m_bus, G_PRIORITY_DEFAULT, d->busCallback, this, nullptr);
0110 
0111     // connect to other signals
0112     g_signal_connect(G_OBJECT(d->m_pipeline), "notify::volume", G_CALLBACK(GstMediaBackendPrivate::playerSignalVolumeChanged), this);
0113     g_signal_connect(G_OBJECT(d->m_pipeline), "notify::mute", G_CALLBACK(GstMediaBackendPrivate::playerSignalMutedChanged), this);
0114     g_signal_connect(G_OBJECT(d->m_scaleTempo), "notify::rate", G_CALLBACK(GstMediaBackendPrivate::playerSignalPlaybackRateChanged), this);
0115 }
0116 
0117 GstMediaBackend::~GstMediaBackend()
0118 {
0119     qCDebug(GstMediaBackendLog) << "GstMediaBackend::~GstMediaBackend()";
0120 
0121     // free memory
0122     if (d->m_msg != nullptr) {
0123         gst_message_unref(d->m_msg);
0124     }
0125     if (d->m_bus != nullptr) {
0126         gst_bus_remove_watch(d->m_bus);
0127         gst_object_unref(d->m_bus);
0128     }
0129     gst_element_set_state(d->m_pipeline, GST_STATE_NULL);
0130     if (d->m_pipeline != nullptr) {
0131         gst_object_unref(d->m_pipeline);
0132     }
0133 }
0134 
0135 KMediaSession::MediaBackends GstMediaBackend::backend() const
0136 {
0137     qCDebug(GstMediaBackendLog) << "GstMediaBackend::backend()";
0138     return KMediaSession::MediaBackends::Gst;
0139 }
0140 
0141 bool GstMediaBackend::muted() const
0142 {
0143     qCDebug(GstMediaBackendLog) << "GstMediaBackend::muted()";
0144     bool isMuted = false;
0145     g_object_get(G_OBJECT(d->m_pipeline), "mute", &isMuted, nullptr);
0146     if (isMuted != d->m_muted) {
0147         d->m_muted = isMuted;
0148     }
0149     return d->m_muted;
0150 }
0151 
0152 qreal GstMediaBackend::volume() const
0153 {
0154     qCDebug(GstMediaBackendLog) << "GstMediaBackend::volume()";
0155     gdouble rawVolume = 1.0;
0156     g_object_get(G_OBJECT(d->m_pipeline), "volume", &rawVolume, nullptr);
0157     qreal volume = static_cast<qreal>(QAudio::convertVolume(rawVolume, QAudio::LinearVolumeScale, QAudio::LogarithmicVolumeScale)) * 100.0;
0158 
0159     if (volume < 0.01) {
0160         volume = 100.0;
0161     }
0162 
0163     if (abs(volume - d->m_volume) > 0.01) {
0164         d->m_volume = volume;
0165     }
0166     return d->m_volume;
0167 }
0168 
0169 QUrl GstMediaBackend::source() const
0170 {
0171     qCDebug(GstMediaBackendLog) << "GstMediaBackend::source()";
0172     return d->m_source;
0173 }
0174 
0175 KMediaSession::MediaStatus GstMediaBackend::mediaStatus() const
0176 {
0177     qCDebug(GstMediaBackendLog) << "GstMediaBackend::mediaStatus()";
0178     return d->m_mediaStatus;
0179 }
0180 
0181 KMediaSession::PlaybackState GstMediaBackend::playbackState() const
0182 {
0183     qCDebug(GstMediaBackendLog) << "GstMediaBackend::playbackState()";
0184     GstState state, pending_state;
0185     gst_element_get_state(d->m_pipeline, &state, &pending_state, static_cast<GstClockTime>(1e9));
0186     qCDebug(GstMediaBackendLog) << "Current state is" << gst_element_state_get_name(state);
0187     switch (state) {
0188     case GST_STATE_PLAYING:
0189         if (!d->m_timer->isActive()) {
0190             d->m_timer->start(d->m_notifyInterval);
0191         }
0192         return KMediaSession::PlaybackState::PlayingState;
0193         break;
0194     case GST_STATE_PAUSED:
0195         if (d->m_timer->isActive()) {
0196             d->m_timer->stop();
0197         }
0198         return KMediaSession::PlaybackState::PausedState;
0199         break;
0200     case GST_STATE_VOID_PENDING:
0201     case GST_STATE_NULL:
0202     case GST_STATE_READY:
0203     default:
0204         if (d->m_timer->isActive()) {
0205             d->m_timer->stop();
0206         }
0207         d->m_rate = 1.0;
0208         return KMediaSession::PlaybackState::StoppedState;
0209         break;
0210     }
0211     return KMediaSession::PlaybackState::StoppedState;
0212 }
0213 
0214 qreal GstMediaBackend::playbackRate() const
0215 {
0216     qCDebug(GstMediaBackendLog) << "GstMediaBackend::playbackRate()";
0217 
0218     return d->m_rate;
0219 }
0220 
0221 KMediaSession::Error GstMediaBackend::error() const
0222 {
0223     qCDebug(GstMediaBackendLog) << "GstMediaBackend::error()";
0224     return d->m_error;
0225 }
0226 
0227 qint64 GstMediaBackend::duration() const
0228 {
0229     qCDebug(GstMediaBackendLog) << "GstMediaBackend::duration()";
0230     gint64 rawDuration = 0;
0231 
0232     if (d->m_pipeline) {
0233         gst_element_query_duration(d->m_pipeline, GST_FORMAT_TIME, &rawDuration);
0234     }
0235     d->m_duration = static_cast<qint64>(rawDuration) / 1000000;
0236     qCDebug(GstMediaBackendLog) << "duration: " << d->m_duration;
0237 
0238     return d->m_duration;
0239 }
0240 
0241 qint64 GstMediaBackend::position() const
0242 {
0243     qCDebug(GstMediaBackendLog) << "GstMediaBackend::position()";
0244 
0245     gint64 position = 0;
0246 
0247     if (d->m_pipeline && d->m_playbackState != KMediaSession::PlaybackState::StoppedState) {
0248         gst_element_query_position(d->m_pipeline, GST_FORMAT_TIME, &position);
0249     }
0250     qCDebug(GstMediaBackendLog) << "position:" << position / 1000000;
0251 
0252     if (d->m_seekPending) {
0253         if (abs(position / 1000000 - d->m_positionAfterSeek) < 1000) {
0254             d->m_seekPending = false;
0255             d->m_positionBeforeSeek = 0;
0256             d->m_positionAfterSeek = 0;
0257         } else {
0258             qCDebug(GstMediaBackendLog) << "but reporting position:" << d->m_positionBeforeSeek << "due to pending seek";
0259             return d->m_positionBeforeSeek;
0260         }
0261     }
0262 
0263     return position / 1000000;
0264 }
0265 
0266 bool GstMediaBackend::seekable() const
0267 {
0268     qCDebug(GstMediaBackendLog) << "GstMediaBackend::seekable()";
0269     switch (d->m_playbackState) {
0270     case KMediaSession::PlaybackState::PlayingState:
0271     case KMediaSession::PlaybackState::PausedState:
0272         d->m_seekable = true;
0273         break;
0274     case KMediaSession::PlaybackState::StoppedState:
0275     default:
0276         d->m_seekable = false;
0277         break;
0278     }
0279     return d->m_seekable;
0280 }
0281 
0282 void GstMediaBackend::setMuted(bool muted)
0283 {
0284     qCDebug(GstMediaBackendLog) << "GstMediaBackend::setMuted(" << muted << ")";
0285     g_object_set(G_OBJECT(d->m_pipeline), "mute", muted, nullptr);
0286     if (d->m_muted != muted) {
0287         d->m_muted = muted;
0288         QTimer::singleShot(0, this, [this]() {
0289             Q_EMIT mutedChanged(d->m_muted);
0290         });
0291     }
0292 }
0293 
0294 void GstMediaBackend::setVolume(qreal volume)
0295 {
0296     qCDebug(GstMediaBackendLog) << "GstMediaBackend::setVolume(" << volume << ")";
0297     if (abs(d->m_volume - volume) > 0.01) {
0298         g_object_set(G_OBJECT(d->m_pipeline),
0299                      "volume",
0300                      static_cast<gdouble>(QAudio::convertVolume(volume / 100.0, QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale)),
0301                      nullptr);
0302         d->m_volume = volume;
0303         QTimer::singleShot(0, this, [this]() {
0304             Q_EMIT volumeChanged(d->m_volume);
0305         });
0306     }
0307 }
0308 
0309 void GstMediaBackend::setSource(const QUrl &source)
0310 {
0311     qCDebug(GstMediaBackendLog) << "GstMediaBackend::setSource(" << source << ")";
0312 
0313     if (playbackState() != KMediaSession::PlaybackState::StoppedState) {
0314         stop();
0315     }
0316 
0317     gst_element_set_state(d->m_pipeline, GST_STATE_NULL);
0318     d->m_seekPending = false;
0319     d->m_positionBeforeSeek = 0;
0320     d->m_positionAfterSeek = 0;
0321     d->m_duration = 0;
0322     // TODO: restore playbackRate of previous source?
0323 
0324     d->m_mediaStatus = KMediaSession::MediaStatus::LoadingMedia;
0325     Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0326 
0327     g_object_set(G_OBJECT(d->m_pipeline), "uri", source.toEncoded().constData(), nullptr);
0328 
0329     if (d->m_error != KMediaSession::Error::NoError) {
0330         d->m_error = KMediaSession::Error::NoError;
0331         Q_EMIT errorChanged(d->m_error);
0332     }
0333 
0334     if (source.isEmpty()) {
0335         d->m_mediaStatus = KMediaSession::MediaStatus::NoMedia;
0336         Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0337     } else {
0338         d->m_mediaStatus = KMediaSession::MediaStatus::LoadedMedia;
0339         Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0340 
0341         gst_element_set_state(d->m_pipeline, GST_STATE_PAUSED);
0342 
0343         d->m_mediaStatus = KMediaSession::MediaStatus::BufferedMedia;
0344         Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0345     }
0346     d->m_source = source;
0347     Q_EMIT sourceChanged(source);
0348 
0349     QTimer::singleShot(0, this, [this]() {
0350         Q_EMIT volumeChanged(volume());
0351         Q_EMIT mutedChanged(muted());
0352     });
0353 }
0354 
0355 void GstMediaBackend::setPosition(qint64 position)
0356 {
0357     qCDebug(GstMediaBackendLog) << "GstMediaBackend::setPosition(" << position << ")";
0358 
0359     d->m_positionBeforeSeek = this->position();
0360     d->m_positionAfterSeek = position;
0361     d->m_seekPending = true;
0362 
0363     qint64 from = d->m_rate > 0 ? position : 0;
0364     qint64 to = d->m_rate > 0 ? duration() : position;
0365 
0366     GstSeekFlags seekFlags = GstSeekFlags(GST_SEEK_FLAG_FLUSH);
0367 
0368     gst_element_seek(d->m_pipeline, d->m_rate, GST_FORMAT_TIME, seekFlags, GST_SEEK_TYPE_SET, from * 1000000, GST_SEEK_TYPE_SET, to * 1000000);
0369 
0370     qCDebug(GstMediaBackendLog) << "Seeking: " << from << to;
0371 }
0372 
0373 void GstMediaBackend::setPlaybackRate(qreal rate)
0374 {
0375     qCDebug(GstMediaBackendLog) << "GstMediaBackend::setPlaybackRate(" << rate << ")";
0376     qint64 currentPosition = position();
0377     qint64 from = rate > 0 ? currentPosition : 0;
0378     qint64 to = rate > 0 ? duration() : currentPosition;
0379     gst_element_seek(d->m_pipeline,
0380                      rate,
0381                      GST_FORMAT_TIME,
0382                      GstSeekFlags(GST_SEEK_FLAG_FLUSH),
0383                      GST_SEEK_TYPE_SET,
0384                      from * 1000000,
0385                      GST_SEEK_TYPE_SET,
0386                      to * 1000000);
0387     if (!qFuzzyCompare(rate, d->m_rate)) {
0388         d->m_rate = rate;
0389         QTimer::singleShot(0, this, [this]() {
0390             Q_EMIT playbackRateChanged(d->m_rate);
0391         });
0392     }
0393 }
0394 
0395 void GstMediaBackend::pause()
0396 {
0397     qCDebug(GstMediaBackendLog) << "GstMediaBackend::pause()";
0398     gst_element_set_state(d->m_pipeline, GST_STATE_PAUSED);
0399     d->m_timer->stop();
0400 }
0401 
0402 void GstMediaBackend::play()
0403 {
0404     qCDebug(GstMediaBackendLog) << "GstMediaBackend::play()";
0405     gst_element_set_state(d->m_pipeline, GST_STATE_PLAYING);
0406     d->m_timer->start(d->m_notifyInterval);
0407     QTimer::singleShot(0, this, [this]() {
0408         d->m_mediaStatus = KMediaSession::MediaStatus::BufferedMedia;
0409         Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0410     });
0411 }
0412 
0413 void GstMediaBackend::stop()
0414 {
0415     qCDebug(GstMediaBackendLog) << "GstMediaBackend::stop()";
0416     d->m_seekPending = false;
0417     d->m_positionBeforeSeek = 0;
0418     d->m_positionAfterSeek = 0;
0419 
0420     gst_element_set_state(d->m_pipeline, GST_STATE_READY);
0421     d->m_timer->stop();
0422 }
0423 
0424 void GstMediaBackend::handleMessage(GstMessage *message)
0425 {
0426     qCDebug(GstSignalsLog) << "GstMediaBackend::handleMessage(" << message << ")";
0427     qCDebug(GstSignalsLog) << "message type" << gst_message_type_get_name(message->type);
0428     if (message->type == GST_MESSAGE_ASYNC_DONE) {
0429         QTimer::singleShot(0, this, [this]() {
0430             Q_EMIT durationChanged(duration());
0431         });
0432     } else if (message->type == GST_MESSAGE_STATE_CHANGED) {
0433         GstState rawOldState, rawState, rawPendingState;
0434         gst_message_parse_state_changed(message, &rawOldState, &rawState, &rawPendingState);
0435         KMediaSession::PlaybackState newState;
0436         switch (rawState) {
0437         case GST_STATE_PLAYING:
0438             newState = KMediaSession::PlaybackState::PlayingState;
0439             break;
0440         case GST_STATE_PAUSED:
0441             newState = KMediaSession::PlaybackState::PausedState;
0442             break;
0443         case GST_STATE_VOID_PENDING:
0444         case GST_STATE_NULL:
0445         case GST_STATE_READY:
0446         default:
0447             newState = KMediaSession::PlaybackState::StoppedState;
0448             d->m_seekPending = false;
0449             d->m_positionBeforeSeek = 0;
0450             d->m_positionAfterSeek = 0;
0451             break;
0452         }
0453         if (newState != d->m_playbackState) {
0454             d->m_playbackState = newState;
0455             QTimer::singleShot(0, this, [this, newState]() {
0456                 Q_EMIT playbackStateChanged(newState);
0457                 Q_EMIT positionChanged(position());
0458                 d->playerSeekableChanged(this);
0459             });
0460         }
0461     } else if (message->type == GST_MESSAGE_BUFFERING) {
0462         gint bufferPercent;
0463         gst_message_parse_buffering(message, &bufferPercent);
0464         KMediaSession::MediaStatus newStatus;
0465         if (bufferPercent < 100) {
0466             newStatus = KMediaSession::MediaStatus::BufferingMedia;
0467         } else {
0468             newStatus = KMediaSession::MediaStatus::BufferedMedia;
0469         }
0470         if (d->m_mediaStatus != newStatus) {
0471             QTimer::singleShot(0, this, [this]() {
0472                 Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0473             });
0474         }
0475     } else if (message->type == GST_MESSAGE_EOS) {
0476         KMediaSession::MediaStatus newStatus = KMediaSession::MediaStatus::EndOfMedia;
0477         if (newStatus != d->m_mediaStatus) {
0478             stop();
0479             d->m_mediaStatus = newStatus;
0480             QTimer::singleShot(0, this, [this]() {
0481                 Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0482             });
0483         }
0484     } else if (message->type == GST_MESSAGE_DURATION_CHANGED) {
0485         if (d->m_duration != duration()) {
0486             // calling duration will already update d->m_duration as side-effect
0487             QTimer::singleShot(0, this, [this]() {
0488                 Q_EMIT durationChanged(d->m_duration);
0489             });
0490         }
0491     } else if (message->type == GST_MESSAGE_TAG) {
0492         GstTagList *tags = nullptr;
0493         gst_message_parse_tag(message, &tags);
0494 
0495         d->parseMetaData(tags);
0496         gst_tag_list_unref(tags);
0497     } else if (message->type == GST_MESSAGE_ERROR) {
0498         GError *err = nullptr;
0499         gchar *dbg_info = nullptr;
0500 
0501         gst_message_parse_error(message, &err, &dbg_info);
0502         // TODO: implement more error parsing
0503         if (err->domain == GST_STREAM_ERROR && err->code == GST_STREAM_ERROR_CODEC_NOT_FOUND) {
0504             d->m_error = KMediaSession::Error::FormatError;
0505         } else {
0506             d->m_error = KMediaSession::Error::ResourceError;
0507         }
0508         d->m_mediaStatus = KMediaSession::MediaStatus::InvalidMedia;
0509         QTimer::singleShot(0, this, [this]() {
0510             Q_EMIT mediaStatusChanged(d->m_mediaStatus);
0511             Q_EMIT errorChanged(d->m_error);
0512         });
0513         g_error_free(err);
0514         g_free(dbg_info);
0515     }
0516     gst_message_unref(message);
0517 }
0518 
0519 void GstMediaBackend::timerUpdate()
0520 {
0521     QTimer::singleShot(0, this, [this]() {
0522         Q_EMIT positionChanged(position());
0523     });
0524 }
0525 
0526 void GstMediaBackendPrivate::playerSignalVolumeChanged(GObject *o, GParamSpec *p, gpointer d)
0527 {
0528     qCDebug(GstSignalsLog) << "GstMediaBackendPrivate::playerSignalVolumeChanged()";
0529     Q_UNUSED(o);
0530     Q_UNUSED(p);
0531     GstMediaBackend *gstMediaBackend = reinterpret_cast<GstMediaBackend *>(d);
0532     QTimer::singleShot(0, gstMediaBackend, [gstMediaBackend]() {
0533         Q_EMIT gstMediaBackend->volumeChanged(gstMediaBackend->volume());
0534     });
0535 }
0536 
0537 void GstMediaBackendPrivate::playerSignalMutedChanged(GObject *o, GParamSpec *p, gpointer d)
0538 {
0539     qCDebug(GstSignalsLog) << "GstMediaBackendPrivate::playerSignalMutedChanged()";
0540     Q_UNUSED(o);
0541     Q_UNUSED(p);
0542     GstMediaBackend *gstMediaBackend = reinterpret_cast<GstMediaBackend *>(d);
0543     QTimer::singleShot(0, gstMediaBackend, [gstMediaBackend]() {
0544         Q_EMIT gstMediaBackend->mutedChanged(gstMediaBackend->muted());
0545     });
0546 }
0547 
0548 void GstMediaBackendPrivate::playerSignalPlaybackRateChanged(GObject *o, GParamSpec *p, gpointer d)
0549 {
0550     qCDebug(GstSignalsLog) << "GstMediaBackendPrivate::playerSignalPlaybackRateChanged()";
0551     Q_UNUSED(o);
0552     Q_UNUSED(p);
0553     GstMediaBackend *gstMediaBackend = reinterpret_cast<GstMediaBackend *>(d);
0554     QTimer::singleShot(0, gstMediaBackend, [gstMediaBackend]() {
0555         gdouble rawRate;
0556         g_object_get(gstMediaBackend->d->m_scaleTempo, "rate", &rawRate, nullptr);
0557 
0558         qreal rate = static_cast<qreal>(rawRate);
0559         if (qFuzzyCompare(rate, 0.0)) {
0560             rate = 1.0;
0561         }
0562 
0563         if (!qFuzzyCompare(rate, gstMediaBackend->d->m_rate)) {
0564             gstMediaBackend->d->m_rate = rate;
0565             Q_EMIT gstMediaBackend->playbackRateChanged(gstMediaBackend->playbackRate());
0566         }
0567     });
0568 }
0569 
0570 void GstMediaBackendPrivate::playerSeekableChanged(GstMediaBackend *backend)
0571 {
0572     qCDebug(GstMediaBackendLog) << "GstMediaBackendPrivate::playerSeekableChanged()";
0573     bool oldSeekable = m_seekable;
0574     bool newSeekable = backend->seekable(); // this will change m_seekable
0575     if (newSeekable != oldSeekable) {
0576         Q_EMIT backend->seekableChanged(m_seekable);
0577         if (newSeekable && !qFuzzyCompare(m_rate, 1.0)) {
0578             backend->setPlaybackRate(m_rate);
0579         }
0580     }
0581 }
0582 
0583 void GstMediaBackendPrivate::parseMetaData(GstTagList *tags)
0584 {
0585     qCDebug(GstMediaBackendLog) << "GstMediaBackendPrivate::parseMetaData()";
0586     // qCDebug(GstSignalsLog) << "dump of all tag info:" << gst_tag_list_to_string(tags);
0587 
0588     char *rawString;
0589     GstSample *sample;
0590 
0591     if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &rawString)) {
0592         QString title = QString::fromUtf8(rawString);
0593         if (m_kMediaSession->metaData()->title().isEmpty()) {
0594             m_kMediaSession->metaData()->setTitle(title);
0595         }
0596         g_free(rawString);
0597     }
0598     if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &rawString)) {
0599         QString artist = QString::fromUtf8(rawString);
0600         if (m_kMediaSession->metaData()->artist().isEmpty()) {
0601             m_kMediaSession->metaData()->setArtist(artist);
0602         }
0603         g_free(rawString);
0604     }
0605     if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &rawString)) {
0606         QString album = QString::fromUtf8(rawString);
0607         if (m_kMediaSession->metaData()->album().isEmpty()) {
0608             m_kMediaSession->metaData()->setAlbum(album);
0609         }
0610         g_free(rawString);
0611     }
0612     if (gst_tag_list_get_sample(tags, GST_TAG_IMAGE, &sample)) {
0613         GstBuffer *buffer = gst_sample_get_buffer(sample);
0614         gsize offset = 0;
0615         gsize dest_size;
0616         gpointer dest;
0617         QByteArray rawImage;
0618         gst_buffer_extract_dup(buffer, offset, gst_buffer_get_size(buffer), &dest, &dest_size);
0619         for (int i = 0; i < static_cast<int>(dest_size); i++) {
0620             rawImage.append(reinterpret_cast<const char *>(dest)[i]);
0621         }
0622         m_imageCacheDir = std::make_unique<QTemporaryDir>();
0623         if (m_imageCacheDir->isValid()) {
0624             QString filePath = m_imageCacheDir->path() + QStringLiteral("/coverimage");
0625 
0626             bool success = QImage::fromData(rawImage).save(filePath, "PNG");
0627 
0628             if (success) {
0629                 QString localFilePath = QStringLiteral("file://") + filePath;
0630                 m_kMediaSession->metaData()->setArtworkUrl(QUrl(localFilePath));
0631             }
0632         }
0633         gst_sample_unref(sample);
0634     }
0635 }