From 0174e7784db6cac74fae86d00b6fc848f05ce2ee Mon Sep 17 00:00:00 2001 From: Kevin Whitaker Date: Mon, 8 Apr 2019 15:58:34 -0400 Subject: [PATCH] Finish implementing basic play, engine, and service. --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 1 + src/minimediaplayer.cpp | 196 ++++++++++++++++++++++++++++++++++++-- src/minimediaplayer.h | 50 ++++++---- src/simplecastengine.cpp | 48 ++++++++-- src/simplecastengine.h | 10 ++ src/simplecastservice.cpp | 41 +++++++- src/simplecastservice.h | 16 +--- 8 files changed, 314 insertions(+), 50 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aba6555..7350cce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(FeatureSummary) -find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core) +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Multimedia) find_package(KF5 ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 003e1a1..94850f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ kcoreaddons_desktop_to_json(plasma_engine_simplecast plasma-dataengine-simplecas target_link_libraries( plasma_engine_simplecast Qt5::Core + Qt5::Multimedia KF5::Plasma KF5::Service KF5::I18n diff --git a/src/minimediaplayer.cpp b/src/minimediaplayer.cpp index 1bfebcb..8a90239 100644 --- a/src/minimediaplayer.cpp +++ b/src/minimediaplayer.cpp @@ -1,12 +1,11 @@ /* - * Simple Media Player that runs off a playlist. * Copyright (C) 2019 Kevin Whitaker * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + *ByteArray(jsonTrack) * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the @@ -17,17 +16,202 @@ */ #include "minimediaplayer.h" +#include +#include +#include MiniMediaPlayer::MiniMediaPlayer(QObject *parent) : QObject(parent) { + m_player = new QMediaPlayer(this,QMediaPlayer::StreamPlayback); + connect(m_player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)),this,SLOT(mediaStatusChanged(QMediaPlayer::MediaStatus))); + connect(m_player,SIGNAL(stateChanged(QMediaPlayer::State)),this,SLOT(mediaStateChanged(QMediaPlayer::State))); + connect(m_player,SIGNAL(durationChanged(qint64)),this,SLOT(durationChanged(qint64))); + connect(m_player,SIGNAL(positionChanged(qint64)),this,SLOT(positionChanged(qint64))); +} + +bool MiniMediaPlayer::isPlaying() +{ + return m_player->state() == QMediaPlayer::State::PlayingState; +} + +bool MiniMediaPlayer::nextTrack() +{ + if(playlist.size() > 1) + { + playlist.pop_front(); + m_player->stop(); + m_player->setMedia(getStreamUrlFromUrl(playlist.first().second)); + emit playlistChanged(getTrackNames()); + return true; + } + return false; +} + +bool MiniMediaPlayer::playPauseTrack(bool playing) +{ + if(m_player->state() == QMediaPlayer::State::PlayingState && !playing) + { + m_player->pause(); + return true; + } + else if(m_player->state() == QMediaPlayer::State::PausedState && playing) + { + m_player->play(); + return true; + } + return false; +} + +QList MiniMediaPlayer::getTrackNames() +{ + QList playlistTitles; + for(QPair item: playlist) + { + playlistTitles.append(item.first); + } + return playlistTitles; +} + +QMap MiniMediaPlayer::currentState() +{ + QMap state; + state.insert("duration",m_player->duration()); + state.insert("position",m_player->position()); + state.insert("state",isPlaying()); + return state; +} + +void MiniMediaPlayer::durationChanged(qint64 length) +{ + emit trackDurationChanged(length); +} + +void MiniMediaPlayer::positionChanged(qint64 position) +{ + emit trackPositionChanged(position); } -QMediaPlaylist* MiniMediaPlayer::playlist() const +bool MiniMediaPlayer::addURLToPlaylist(QUrl mediaUrl) { - return m_playlist; + if(isValidMediaUrl(mediaUrl)) + { + bool shouldStartPlaying = false; + if(playlist.size() == 0) shouldStartPlaying = true; + QList> tracks = getStreamTitlesAndUrlsFromUrl(mediaUrl); + playlist.append(tracks); + emit playlistChanged(getTrackNames()); + if(shouldStartPlaying && playlist.size() > 0) + { + m_player->setMedia(getStreamUrlFromUrl(playlist.first().second)); + } + if(tracks.size() > 0) + { + return true; + } + } + return false; } -QMediaPlayer* MiniMediaPlayer::player() const +QUrl MiniMediaPlayer::getStreamUrlFromUrl(QUrl mediaUrl) { - return m_player; + //Run youtube-dl on Url and return Url given back. + QProcess youtubedl; + youtubedl.start("youtube-dl -f bestaudio -g "+mediaUrl.toString()); + if(youtubedl.waitForFinished(-1)) + { + QByteArray out = youtubedl.readAllStandardOutput(); + QString outStr = QString::fromStdString(out.toStdString()); + for(QString track:outStr.split('\n')) + { + qDebug() << "Found URL stream:" << track; + return QUrl(track); + } + } + return QUrl(); } + +bool MiniMediaPlayer::isValidMediaUrl(QUrl mediaUrl) +{ + //Only support youtube and bandcamp for now. + if(mediaUrl.host().endsWith("youtube.com") || mediaUrl.host().endsWith("bandcamp.com")) + { + //TODO: check more than host? + return true; + } + return false; +} + +void MiniMediaPlayer::mediaStateChanged(QMediaPlayer::State state) +{ + if(state == QMediaPlayer::State::PlayingState) + { + emit playStateChanged(true); + } + else + { + emit playStateChanged(false); + } +} + +void MiniMediaPlayer::mediaStatusChanged(QMediaPlayer::MediaStatus status) +{ + //Make sure to load next track once track has ended. + if(status == QMediaPlayer::MediaStatus::EndOfMedia) + { + //Load next track + nextTrack(); + } + else if(status == QMediaPlayer::MediaStatus::InvalidMedia) + { + qWarning() << "Unable to play track."; + } + else if(status == QMediaPlayer::MediaStatus::LoadedMedia) + { + //Start playing media + m_player->play(); + } +} + +QList> MiniMediaPlayer::getStreamTitlesAndUrlsFromUrl(QUrl mediaUrl) +{ + QList> tracks; + //Run youtube-dl on Url and return json given back. + QProcess youtubedl; + youtubedl.start("youtube-dl -f bestaudio -j "+mediaUrl.toString()); + if(youtubedl.waitForFinished(-1)) + { + //If there is anything in error output, error must have occured. + if(youtubedl.readAllStandardError().size()== 0) + { + QByteArray out = youtubedl.readAllStandardOutput(); + QString outStr = QString::fromStdString(out.toStdString()); + for(QString jsonTrack:outStr.split('\n')) + { + //Parse urls and titles from json list + QJsonDocument info = QJsonDocument::fromJson(jsonTrack.toUtf8()); + if(!info.isNull() && info.isObject()) + { + QPair track; + QJsonObject jsonTrackInfo = info.object(); + track.first = jsonTrackInfo["title"].toString(); + track.second = QUrl(jsonTrackInfo["webpage_url"].toString()); + tracks.append(track); + } + } + } + else + { + qWarning() << "Url may be invalid."; + } + } + return tracks; +} + + + + + + + + + diff --git a/src/minimediaplayer.h b/src/minimediaplayer.h index a761218..6c5b1b5 100644 --- a/src/minimediaplayer.h +++ b/src/minimediaplayer.h @@ -1,5 +1,4 @@ /* - * Simple Media Player that runs off a playlist. * Copyright (C) 2019 Kevin Whitaker * * This program is free software: you can redistribute it and/or modify @@ -21,36 +20,49 @@ #include #include -#include /** - * @todo write docs + * Mini Media Player is a simple class that wraps some wrappers around Qt's Multimedia classes for adding and playing media from a playlist. Also parses URLs to be added to playlist using youtube-dl and makes sure they are valid. */ class MiniMediaPlayer : public QObject { Q_OBJECT - Q_PROPERTY(QMediaPlaylist playlist READ playlist) - Q_PROPERTY(QMediaPlayer player READ player) public: - /** - * @todo write docs - */ MiniMediaPlayer(QObject *parent = 0); + + bool addURLToPlaylist(QUrl mediaUrl); - /** - * @return the playlist - */ - QMediaPlaylist *playlist() const; - - /** - * @return the player - */ - QMediaPlayer *player() const; - + QList getTrackNames(); + + bool isPlaying(); + + QMap currentState(); + + bool nextTrack(); + + bool playPauseTrack(bool playing); + +signals: + void playStateChanged(bool isPlaying); + void playlistChanged(QStringList trackTitles); + void trackDurationChanged(qint64 length); + void trackPositionChanged(qint64 position); + +private slots: + void mediaStatusChanged(QMediaPlayer::MediaStatus status); + void mediaStateChanged(QMediaPlayer::State state); + void durationChanged(qint64 length); + void positionChanged(qint64 position); + private: - QMediaPlaylist *m_playlist; QMediaPlayer *m_player; + QList> playlist; + + bool isValidMediaUrl(QUrl mediaUrl); + QUrl getStreamUrlFromUrl(QUrl mediaUrl); + QList> getStreamTitlesAndUrlsFromUrl(QUrl mediaUrl); + }; #endif // MINIMEDIAPLAYER_H diff --git a/src/simplecastengine.cpp b/src/simplecastengine.cpp index 3cff0a7..13af3c6 100644 --- a/src/simplecastengine.cpp +++ b/src/simplecastengine.cpp @@ -22,25 +22,55 @@ #include "simplecastservice.h" SimpleCastEngine::SimpleCastEngine(QObject *parent, const QVariantList &args) - : Plasma::DataEngine(parent,args) + : Plasma::DataEngine(parent,args), player(new MiniMediaPlayer(this)) { Q_UNUSED(args) setMinimumPollingInterval(333); + connect(player,SIGNAL(playStateChanged(bool)),this,SLOT(playStateChanged(bool))); + connect(player,SIGNAL(trackDurationChanged(qint64)),this,SLOT(durationChanged(qint64))); + connect(player,SIGNAL(trackPositionChanged(qint64)),this,SLOT(positionChanged(qint64))); + connect(player,SIGNAL(playlistChanged(QStringList)),this,SLOT(playlistChanged(QStringList))); +} + +void SimpleCastEngine::durationChanged(qint64 length) +{ + setData("CurrentState","trackDuration",length); +} + +void SimpleCastEngine::playStateChanged(bool isPlaying) +{ + setData("CurrentState","playingState",isPlaying); +} + +void SimpleCastEngine::positionChanged(qint64 position) +{ + setData("CurrentState","trackPosition",position); } bool SimpleCastEngine::sourceRequestEvent(const QString& source) { - setData(source, "you found me"); - return true; - //return updateSourceEvent(source);//TODO + return updateSourceEvent(source); +} + +void SimpleCastEngine::playlistChanged(QStringList playlist) +{ + setData("Playlist",playlist); } bool SimpleCastEngine::updateSourceEvent(const QString& source) { - //Source for current playlist(contains title and url) - //Source for current track time and length, and playing state - setData(source, "you found me again"); - return true;//TODO + if(source == "Playlist") + { + setData(source, QStringList(player->getTrackNames())); + } + else if(source == "CurrentState") + { + QMap state = player->currentState(); + setData(source,"trackDuration",state["duration"]); + setData(source,"trackPosition",state["position"]); + setData(source,"playingState",state["state"]); + } + return true; } QStringList SimpleCastEngine::sources() const @@ -52,7 +82,7 @@ Plasma::Service * SimpleCastEngine::serviceForSource(const QString& source) { if(source == "CurrentState") { - return new SimpleCastService(source, this); + return new SimpleCastService(source, player, this); } return Plasma::DataEngine::serviceForSource(source); } diff --git a/src/simplecastengine.h b/src/simplecastengine.h index 23a5d62..8972921 100644 --- a/src/simplecastengine.h +++ b/src/simplecastengine.h @@ -24,7 +24,11 @@ #include #include +#include "minimediaplayer.h" +/** + * Simple Cast Engine is a Plasma DataEngine that conveys that status on the media player casting to to other code like Plasmoids. + */ class SimpleCastEngine : public Plasma::DataEngine { Q_OBJECT @@ -33,9 +37,15 @@ public: QStringList sources() const override; Plasma::Service *serviceForSource(const QString &source) override; +private slots: + void durationChanged(qint64 length); + void positionChanged(qint64 position); + void playStateChanged(bool isPlaying); + void playlistChanged(QStringList playlist); protected: bool sourceRequestEvent(const QString &source) override; bool updateSourceEvent(const QString &source) override; + MiniMediaPlayer *player; }; diff --git a/src/simplecastservice.cpp b/src/simplecastservice.cpp index 9fb123c..1abfefd 100644 --- a/src/simplecastservice.cpp +++ b/src/simplecastservice.cpp @@ -1,6 +1,5 @@ /* - * - * Copyright (C) 2019 + * Copyright (C) 2019 Kevin Whitaker * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,8 +16,42 @@ */ #include "simplecastservice.h" +#include -SimpleCastService::SimpleCastService(const QString &destination, QObject *parent) : Plasma::Service(parent) +class SimpleCastJob : public Plasma::ServiceJob +{ + Q_OBJECT +public: + SimpleCastJob(const QString& destination, const QString& operation, MiniMediaPlayer *player, const QVariantMap& parameters, QObject* parent = 0) : Plasma::ServiceJob(destination,operation,parameters,parent),player(player) {} + + virtual void start() override + { + const auto operation = operationName(); + const auto paramaters = parameters(); + if(operation == "changePlayState") + { + if(paramaters.contains("playing")) + { + setResult(player->playPauseTrack(paramaters["playing"].toBool())); + } + } + else if(operation == "skipToNextTrack") + { + setResult(player->nextTrack()); + } + else if(operation == "addURLToPlaylist") + { + if(paramaters.contains("URL")) + { + setResult(player->addURLToPlaylist(paramaters["URL"].toUrl())); + } + } + } +private: + MiniMediaPlayer *player; +}; + +SimpleCastService::SimpleCastService(const QString &destination,MiniMediaPlayer *player, QObject *parent) : Plasma::Service(parent), player(player) { setName("simplecastcontrol"); } @@ -30,7 +63,7 @@ SimpleCastService::~SimpleCastService() Plasma::ServiceJob* SimpleCastService::createJob(const QString& operation, QVariantMap& parameters) { - return NULL; //TODO + return new SimpleCastJob(destination(),operation,player,parameters,this); } //K_EXPORT_PLASMA_SERVICE(org.eyecreate.simplecast, SimpleCastService) diff --git a/src/simplecastservice.h b/src/simplecastservice.h index 0563660..60d03ee 100644 --- a/src/simplecastservice.h +++ b/src/simplecastservice.h @@ -1,6 +1,5 @@ /* - * - * Copyright (C) 2019 + * Copyright (C) 2019 Kevin Whitaker * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,25 +19,20 @@ #define SIMPLECASTSERVICE_H #include +#include "minimediaplayer.h" /** - * @todo write docs + * Simple Cast Service is a Plasma Service that controls the media player casting to. */ class SimpleCastService : public Plasma::Service { Q_OBJECT public: - SimpleCastService(const QString &destination, QObject *parent = 0); + SimpleCastService(const QString &destination, MiniMediaPlayer *player, QObject *parent = 0); ~SimpleCastService(); protected: - /** - * @todo write docs - * - * @param operation TODO - * @param parameters TODO - * @return TODO - */ Plasma::ServiceJob* createJob(const QString& operation, QVariantMap& parameters) override; + MiniMediaPlayer *player; }; -- GitLab