diff --git a/CMakeLists.txt b/CMakeLists.txt index 544e7e499..c8b048f4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ pkg_check_modules(GSTREAMER_APP REQUIRED gstreamer-app-1.0) pkg_check_modules(GSTREAMER_AUDIO REQUIRED gstreamer-audio-1.0) pkg_check_modules(GSTREAMER_BASE REQUIRED gstreamer-base-1.0) pkg_check_modules(GSTREAMER_TAG REQUIRED gstreamer-tag-1.0) +pkg_check_modules(GSTREAMER_PBUTILS REQUIRED gstreamer-pbutils-1.0) pkg_check_modules(LIBGPOD libgpod-1.0>=0.7.92) pkg_check_modules(LIBMTP libmtp>=1.0) pkg_check_modules(LIBMYGPO_QT libmygpo-qt>=1.0.9) @@ -155,6 +156,7 @@ include_directories(${GSTREAMER_APP_INCLUDE_DIRS}) include_directories(${GSTREAMER_AUDIO_INCLUDE_DIRS}) include_directories(${GSTREAMER_BASE_INCLUDE_DIRS}) include_directories(${GSTREAMER_TAG_INCLUDE_DIRS}) +include_directories(${GSTREAMER_PBUTILS_INCLUDE_DIRS}) include_directories(${GLIB_INCLUDE_DIRS}) include_directories(${GLIBCONFIG_INCLUDE_DIRS}) include_directories(${LIBXML_INCLUDE_DIRS}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1afaaf707..76958329b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -311,6 +311,7 @@ set(SOURCES songinfo/songkickconcertwidget.cpp songinfo/songplaystats.cpp songinfo/spotifyimages.cpp + songinfo/streamdiscoverer.cpp songinfo/taglyricsinfoprovider.cpp songinfo/ultimatelyricslyric.cpp songinfo/ultimatelyricsprovider.cpp @@ -358,6 +359,7 @@ set(SOURCES ui/settingsdialog.cpp ui/settingspage.cpp ui/standarditemiconloader.cpp + ui/streamdetailsdialog.cpp ui/systemtrayicon.cpp ui/trackselectiondialog.cpp ui/windows7thumbbar.cpp @@ -603,6 +605,7 @@ set(HEADERS songinfo/songkickconcertwidget.h songinfo/songplaystats.h songinfo/spotifyimages.h + songinfo/streamdiscoverer.h songinfo/taglyricsinfoprovider.h songinfo/ultimatelyricslyric.h songinfo/ultimatelyricsprovider.h @@ -641,6 +644,7 @@ set(HEADERS ui/settingsdialog.h ui/settingspage.h ui/standarditemiconloader.h + ui/streamdetailsdialog.h ui/systemtrayicon.h ui/trackselectiondialog.h ui/windows7thumbbar.h @@ -764,6 +768,7 @@ set(UI ui/organiseerrordialog.ui ui/playbacksettingspage.ui ui/settingsdialog.ui + ui/streamdetailsdialog.ui ui/trackselectiondialog.ui widgets/equalizerslider.ui @@ -1265,6 +1270,7 @@ target_link_libraries(clementine_lib ${GSTREAMER_LIBRARIES} ${GSTREAMER_APP_LIBRARIES} ${GSTREAMER_TAG_LIBRARIES} + ${GSTREAMER_PBUTILS_LIBRARIES} ${QTSINGLEAPPLICATION_LIBRARIES} ${QTSINGLECOREAPPLICATION_LIBRARIES} ${QTIOCOMPRESSOR_LIBRARIES} diff --git a/src/engines/gstengine.cpp b/src/engines/gstengine.cpp index 51d2c5e50..f0d47b4e9 100644 --- a/src/engines/gstengine.cpp +++ b/src/engines/gstengine.cpp @@ -39,6 +39,7 @@ #include #include +#include #include "config.h" #include "devicefinder.h" @@ -146,6 +147,8 @@ bool GstEngine::Init() { void GstEngine::InitialiseGstreamer() { gst_init(nullptr, nullptr); + gst_pb_utils_init(); + #ifdef HAVE_MOODBAR gstfastspectrum_register_static(); #endif diff --git a/src/songinfo/streamdiscoverer.cpp b/src/songinfo/streamdiscoverer.cpp new file mode 100644 index 000000000..812034b30 --- /dev/null +++ b/src/songinfo/streamdiscoverer.cpp @@ -0,0 +1,125 @@ +#include "streamdiscoverer.h" + +#include +#include "core/logging.h" +#include "core/signalchecker.h" +#include "core/waitforsignal.h" + +#include + +const int StreamDiscoverer::kDiscoveryTimeoutS = 10; + +StreamDiscoverer::StreamDiscoverer() : QObject(nullptr) { + // Setting up a discoverer: + discoverer_ = gst_discoverer_new(kDiscoveryTimeoutS * GST_SECOND, NULL); + if (discoverer_ == NULL) { + qLog(Error) << "Error creating discoverer" << endl; + return; + } + + // Connecting its signals: + CHECKED_GCONNECT(discoverer_, "discovered", &OnDiscovered, this); + CHECKED_GCONNECT(discoverer_, "finished", &OnFinished, this); + + // Starting the discoverer process: + gst_discoverer_start(discoverer_); +} + +StreamDiscoverer::~StreamDiscoverer() { + gst_discoverer_stop(discoverer_); + g_object_unref(discoverer_); +} + +void StreamDiscoverer::Discover(const QString& url) { + // Adding the request to discover the url given as a parameter: + qLog(Debug) << "Discover" << url; + if (!gst_discoverer_discover_uri_async(discoverer_, + url.toStdString().c_str())) { + qLog(Error) << "Failed to start discovering" << url << endl; + return; + } + WaitForSignal(this, SIGNAL(DiscoverFinished())); +} + +void StreamDiscoverer::OnDiscovered(GstDiscoverer* discoverer, + GstDiscovererInfo* info, GError* err, + gpointer self) { + StreamDiscoverer* instance = reinterpret_cast(self); + + QString discovered_url(gst_discoverer_info_get_uri(info)); + + GstDiscovererResult result = gst_discoverer_info_get_result(info); + if (result != GST_DISCOVERER_OK) { + QString error_message = GSTdiscovererErrorMessage(result); + qLog(Error) << "Discovery failed:" << error_message << endl; + emit instance->Error( + tr("Error discovering %1: %2").arg(discovered_url).arg(error_message)); + return; + } + + // Get audio streams (we will only care about the first one, which should be + // the only one). + GList* audio_streams = gst_discoverer_info_get_audio_streams(info); + + if (audio_streams != nullptr) { + qLog(Debug) << "Discovery successful" << endl; + // We found a valid audio stream, extracting and saving its info: + GstDiscovererStreamInfo* stream_audio_info = + (GstDiscovererStreamInfo*)g_list_first(audio_streams)->data; + + StreamDetails stream_details; + stream_details.url = discovered_url; + stream_details.bitrate = gst_discoverer_audio_info_get_bitrate( + GST_DISCOVERER_AUDIO_INFO(stream_audio_info)); + stream_details.channels = gst_discoverer_audio_info_get_channels( + GST_DISCOVERER_AUDIO_INFO(stream_audio_info)); + stream_details.depth = gst_discoverer_audio_info_get_depth( + GST_DISCOVERER_AUDIO_INFO(stream_audio_info)); + stream_details.sample_rate = gst_discoverer_audio_info_get_sample_rate( + GST_DISCOVERER_AUDIO_INFO(stream_audio_info)); + + // Human-readable codec name: + GstCaps* stream_caps = + gst_discoverer_stream_info_get_caps(stream_audio_info); + gchar* decoder_description = + gst_pb_utils_get_codec_description(stream_caps); + stream_details.format = (decoder_description == NULL) + ? QString(tr("Unknown")) + : QString(decoder_description); + + gst_caps_unref(stream_caps); + g_free(decoder_description); + + emit instance->DataReady(stream_details); + + } else { + emit instance->Error( + tr("Could not detect an audio stream in %1").arg(discovered_url)); + } + + gst_discoverer_stream_info_list_free(audio_streams); +} + +void StreamDiscoverer::OnFinished(GstDiscoverer* discoverer, gpointer self) { + // The discoverer doesn't have any more urls in its queue. Let the loop know + // it can exit. + StreamDiscoverer* instance = reinterpret_cast(self); + emit instance->DiscoverFinished(); +} + +QString StreamDiscoverer::GSTdiscovererErrorMessage( + GstDiscovererResult result) { + switch (result) { + case (GST_DISCOVERER_URI_INVALID): + return tr("Invalid URL"); + case (GST_DISCOVERER_TIMEOUT): + return tr("Connection timed out"); + case (GST_DISCOVERER_BUSY): + return tr("The discoverer is busy"); + case (GST_DISCOVERER_MISSING_PLUGINS): + return tr("Missing plugins"); + case (GST_DISCOVERER_ERROR): + default: + return tr("Could not get details"); + } +} diff --git a/src/songinfo/streamdiscoverer.h b/src/songinfo/streamdiscoverer.h new file mode 100644 index 000000000..e5016269e --- /dev/null +++ b/src/songinfo/streamdiscoverer.h @@ -0,0 +1,48 @@ +#ifndef STREAMDISCOVERER_H +#define STREAMDISCOVERER_H + +#include + +#include +#include +#include + +struct StreamDetails { + QString url; + QString format; + int bitrate; + int depth; + int channels; + int sample_rate; +}; +Q_DECLARE_METATYPE(StreamDetails) + +class StreamDiscoverer : public QObject { + Q_OBJECT + + public: + StreamDiscoverer(); + ~StreamDiscoverer(); + + void Discover(const QString& url); + +signals: + void DiscoverFinished(); + void DataReady(const StreamDetails& data); + void Error(const QString& message); + + private: + GstDiscoverer* discoverer_; + + static const int kDiscoveryTimeoutS; + + // GstDiscoverer callbacks: + static void OnDiscovered(GstDiscoverer* discoverer, GstDiscovererInfo* info, + GError* err, gpointer instance); + static void OnFinished(GstDiscoverer* discoverer, gpointer instance); + + // Helper to return descriptive error messages: + static QString GSTdiscovererErrorMessage(GstDiscovererResult result); +}; + +#endif // STREAMDISCOVERER_H diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index ba0a22fb9..bfe1676ef 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -101,6 +101,7 @@ #include "smartplaylists/generatormimedata.h" #include "songinfo/artistinfoview.h" #include "songinfo/songinfoview.h" +#include "songinfo/streamdiscoverer.h" #include "transcoder/transcodedialog.h" #include "ui/about.h" #include "ui/addstreamdialog.h" @@ -113,6 +114,7 @@ #include "ui/organiseerrordialog.h" #include "ui/qtsystemtrayicon.h" #include "ui/settingsdialog.h" +#include "ui/streamdetailsdialog.h" #include "ui/systemtrayicon.h" #include "ui/trackselectiondialog.h" #include "ui/windows7thumbbar.h" @@ -173,6 +175,7 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd, tray_icon_(tray_icon), osd_(osd), edit_tag_dialog_(std::bind(&MainWindow::CreateEditTagDialog, this)), + stream_discoverer_(std::bind(&MainWindow::CreateStreamDiscoverer, this)), global_shortcuts_(new GlobalShortcuts(this)), global_search_view_(new GlobalSearchView(app_, this)), library_view_(new LibraryViewContainer(this)), @@ -477,6 +480,8 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd, SLOT(ShowQueueManager())); connect(ui_->action_add_files_to_transcoder, SIGNAL(triggered()), SLOT(AddFilesToTranscoder())); + connect(ui_->action_view_stream_details, SIGNAL(triggered()), + SLOT(DiscoverStreamDetails())); background_streams_->AddAction("Rain", ui_->action_rain); background_streams_->AddAction("Hypnotoad", ui_->action_hypnotoad); @@ -684,6 +689,7 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd, playlist_menu_->addAction(ui_->action_remove_from_playlist); playlist_undoredo_ = playlist_menu_->addSeparator(); playlist_menu_->addAction(ui_->action_edit_track); + playlist_menu_->addAction(ui_->action_view_stream_details); playlist_menu_->addAction(ui_->action_edit_value); playlist_menu_->addAction(ui_->action_renumber_tracks); playlist_menu_->addAction(ui_->action_selection_set_value); @@ -1712,6 +1718,10 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos, // no 'show in browser' action if only streams are selected playlist_open_in_browser_->setVisible(streams != all); + // If exactly one stream is selected, enable the 'show details' action. + ui_->action_view_stream_details->setEnabled(all == 1 && streams == 1); + ui_->action_view_stream_details->setVisible(all == 1 && streams == 1); + bool track_column = (index.column() == Playlist::Column_Track); ui_->action_renumber_tracks->setVisible(editable >= 2 && track_column); ui_->action_selection_set_value->setVisible(editable >= 2 && !track_column); @@ -1882,6 +1892,27 @@ void MainWindow::EditTagDialogAccepted() { app_->playlist_manager()->current()->Save(); } +void MainWindow::DiscoverStreamDetails() { + int row = playlist_menu_index_.row(); + Song song = app_->playlist_manager()->current()->item_at(row)->Metadata(); + + QString url = song.url().toString(); + stream_discoverer_->Discover(url); +} + +void MainWindow::ShowStreamDetails(const StreamDetails& details) { + StreamDetailsDialog stream_details_dialog(this); + + stream_details_dialog.setUrl(details.url); + stream_details_dialog.setFormat(details.format); + stream_details_dialog.setBitrate(details.bitrate); + stream_details_dialog.setChannels(details.channels); + stream_details_dialog.setDepth(details.depth); + stream_details_dialog.setSampleRate(details.sample_rate); + + stream_details_dialog.exec(); +} + void MainWindow::RenumberTracks() { QModelIndexList indexes = ui_->playlist->view()->selectionModel()->selection().indexes(); @@ -2490,6 +2521,14 @@ EditTagDialog* MainWindow::CreateEditTagDialog() { return edit_tag_dialog; } +StreamDiscoverer* MainWindow::CreateStreamDiscoverer() { + StreamDiscoverer* discoverer = new StreamDiscoverer(); + connect(discoverer, SIGNAL(DataReady(StreamDetails)), + SLOT(ShowStreamDetails(StreamDetails))); + connect(discoverer, SIGNAL(Error(QString)), SLOT(ShowErrorDialog(QString))); + return discoverer; +} + void MainWindow::ShowAboutDialog() { about_dialog_->show(); } void MainWindow::ShowTranscodeDialog() { transcode_dialog_->show(); } diff --git a/src/ui/mainwindow.h b/src/ui/mainwindow.h index 2e5d4ec17..d796d39bb 100644 --- a/src/ui/mainwindow.h +++ b/src/ui/mainwindow.h @@ -31,8 +31,10 @@ #include "engines/engine_fwd.h" #include "library/librarymodel.h" #include "playlist/playlistitem.h" +#include "songinfo/streamdiscoverer.h" #include "ui/organisedialog.h" #include "ui/settingsdialog.h" +#include "ui/streamdetailsdialog.h" class About; class AddStreamDialog; @@ -72,6 +74,7 @@ class RipCDDialog; class Song; class SongInfoBase; class SongInfoView; +class StreamDetailsDialog; class SystemTrayIcon; class TagFetcher; class TaskManager; @@ -165,6 +168,8 @@ signals: void PlaylistEditFinished(const QModelIndex& index); void EditTracks(); void EditTagDialogAccepted(); + void DiscoverStreamDetails(); + void ShowStreamDetails(const StreamDetails& details); void RenumberTracks(); void SelectionSetValue(); void EditValue(); @@ -252,6 +257,7 @@ signals: void ShowVisualisations(); SettingsDialog* CreateSettingsDialog(); EditTagDialog* CreateEditTagDialog(); + StreamDiscoverer* CreateStreamDiscoverer(); void OpenSettingsDialog(); void OpenSettingsDialogAtPage(SettingsDialog::Page page); void ShowSongInfoConfig(); @@ -299,6 +305,7 @@ signals: OSD* osd_; Lazy edit_tag_dialog_; Lazy about_dialog_; + Lazy stream_discoverer_; GlobalShortcuts* global_shortcuts_; diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui index 8fd57050e..fce8c3fa3 100644 --- a/src/ui/mainwindow.ui +++ b/src/ui/mainwindow.ui @@ -928,6 +928,11 @@ Remove unavailable tracks from playlist + + + View Stream Details + + true diff --git a/src/ui/streamdetailsdialog.cpp b/src/ui/streamdetailsdialog.cpp new file mode 100644 index 000000000..cab6b879f --- /dev/null +++ b/src/ui/streamdetailsdialog.cpp @@ -0,0 +1,42 @@ +#include "streamdetailsdialog.h" +#include "ui_streamdetailsdialog.h" + +#include + +StreamDetailsDialog::StreamDetailsDialog(QWidget* parent) + : QDialog(parent), ui_(new Ui::StreamDetailsDialog) { + ui_->setupUi(this); +} + +StreamDetailsDialog::~StreamDetailsDialog() {} + +void StreamDetailsDialog::setUrl(const QString& url) { + ui_->url->setText(url); + ui_->url->setCursorPosition(0); +} +void StreamDetailsDialog::setFormat(const QString& format) { + ui_->format->setText(format); +} +void StreamDetailsDialog::setBitrate(int bitrate) { + ui_->bitrate->setText(QString("%1 kbps").arg(bitrate / 1000)); + + // Some bitrates aren't properly reported by GStreamer. + // In that case do not display bitrate information. + ui_->bitrate->setVisible(bitrate != 0); + ui_->bitrate_label->setVisible(bitrate != 0); +} +void StreamDetailsDialog::setChannels(int channels) { + ui_->channels->setText(QString::number(channels)); +} +void StreamDetailsDialog::setDepth(int depth) { + // Right now GStreamer seems to be reporting incorrect numbers for MP3 and AAC + // streams, so we leave that value hidden in the UI. + // ui_->depth->setText(QString("%1 bits").arg(depth)); + ui_->depth->setVisible(false); + ui_->depth_label->setVisible(false); +} +void StreamDetailsDialog::setSampleRate(int sample_rate) { + ui_->sample_rate->setText(QString("%1 Hz").arg(sample_rate)); +} + +void StreamDetailsDialog::Close() { this->close(); } diff --git a/src/ui/streamdetailsdialog.h b/src/ui/streamdetailsdialog.h new file mode 100644 index 000000000..1e10a619c --- /dev/null +++ b/src/ui/streamdetailsdialog.h @@ -0,0 +1,34 @@ +#ifndef STREAMDETAILSDIALOG_H +#define STREAMDETAILSDIALOG_H + +#include + +#include + +namespace Ui { +class StreamDetailsDialog; +} + +class StreamDetailsDialog : public QDialog { + Q_OBJECT + + public: + explicit StreamDetailsDialog(QWidget* parent = 0); + ~StreamDetailsDialog(); + + void setUrl(const QString& url); + void setFormat(const QString& codec); // This is localized, so only for human + // consumption. + void setBitrate(int); + void setDepth(int); + void setChannels(int); + void setSampleRate(int); + + private slots: + void Close(); + + private: + std::unique_ptr ui_; +}; + +#endif // STREAMDETAILSDIALOG_H diff --git a/src/ui/streamdetailsdialog.ui b/src/ui/streamdetailsdialog.ui new file mode 100644 index 000000000..3b6120c9f --- /dev/null +++ b/src/ui/streamdetailsdialog.ui @@ -0,0 +1,148 @@ + + + StreamDetailsDialog + + + + 0 + 0 + 500 + 210 + + + + + 500 + 210 + + + + Stream Details + + + + + + URL + + + + + + + true + + + + + + + Format + + + + + + + + + + + + + + Channels + + + + + + + + + + + + + + Bit rate + + + + + + + + + + + + + + Sample rate + + + + + + + + + + + + + + Depth + + + + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + buttonBox + rejected() + StreamDetailsDialog + close() + + + 299 + 186 + + + 249 + 104 + + + + +