~zanneth/somafm-gtk

5afa791fc628af2a18dac29e36173e062a1fe2ff — James Magahern 3 months ago 5f1370b master
app: Adds MPRIS support for remote control
M CMakeLists.txt => CMakeLists.txt +20 -0
@@ 26,6 26,10 @@ pkg_check_modules(JSONCPP REQUIRED jsoncpp)
link_directories(${JSONCPP_LIBRARY_DIRS})
include_directories(include ${JSONCPP_INCLUDE_DIRS})

pkg_check_modules(DBUSCPP REQUIRED dbus-c++-1 dbus-c++-glib-1)
link_directories(${DBUSCPP_LIBRARY_DIRS})
include_directories(include ${DBUSCPP_INCLUDE_DIRS})

set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)



@@ 34,9 38,25 @@ target_link_libraries(${EXECUTABLE_NAME}
    ${GSTREAMERMM_LIBRARIES}
    ${CURLPP_LIBRARIES}
    ${JSONCPP_LIBRARIES}
    ${DBUSCPP_LIBRARIES}
    Threads::Threads
)

# use dbusxx-xml2cpp to generate dbus proxy classes
find_program(DBUSXX_XML2CPP dbusxx-xml2cpp)
if (NOT DBUSXX_XML2CPP)
    message (FATAL_ERROR "Requires dbusxx-xml2cpp command from dbus-c++-1")
endif()

execute_process(
    COMMAND ${DBUSXX_XML2CPP} ${CMAKE_SOURCE_DIR}/share/org.mpris.MediaPlayer2.Player.xml
        --proxy=${CMAKE_SOURCE_DIR}/generated/mpris_proxy.h
        --adaptor=${CMAKE_SOURCE_DIR}/generated/mpris_adaptor.h
)

# include generated dbus proxy classes
include_directories(${CMAKE_SOURCE_DIR}/generated)

# function for including textual files as header
function(make_includable input_file output_file)
    file(READ ${input_file} content)

A share/org.mpris.MediaPlayer2.Player.xml => share/org.mpris.MediaPlayer2.Player.xml +510 -0
@@ 0,0 1,510 @@
<!DOCTYPE node PUBLIC
'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
<node>

  <!--
      org.mpris.MediaPlayer2:
      @short_description: Media Player Remote Interfacing Specification

      The Media Player Remote Interfacing Specification is a standard
      D-Bus interface which aims to provide a common programmatic API
      for controlling media players.

      This interface implements the methods for querying and providing
      basic control over what is currently playing.
  -->
  <interface name="org.mpris.MediaPlayer2.Player">

  <!--
      Next:

      Skips to the next track in the tracklist.

      If there is no next track (and endless playback and track repeat
      are both off), stop playback.

      If playback is paused or stopped, it remains that way.

      If CanGoNext is false, attempting to call this method should
      have no effect.
  -->
    <method name="Next"/>

  <!--
      Previous:

      Skips to the previous track in the tracklist.

      If there is no previous track (and endless playback and track
      repeat are both off), stop playback.

      If playback is paused or stopped, it remains that way.

      If CanGoPrevious is false, attempting to call this method should
      have no effect.
  -->
    <method name="Previous"/>

  <!--
      Pause:

      Pauses playback.

      If playback is already paused, this has no effect.

      Calling Play after this should cause playback to start again
      from the same position.

      If CanPause is false, attempting to call this method should have
      no effect.
  -->
    <method name="Pause"/>

  <!--
      PlayPause:

      Pauses playback.

      If playback is already paused, resumes playback.

      If playback is stopped, starts playback.

      If CanPause is false, attempting to call this method should have
      no effect and raise an error.
  -->
    <method name="PlayPause"/>

  <!--
      Stop:

      Stops playback.

      If playback is already stopped, this has no effect.

      Calling Play after this should cause playback to start again
      from the beginning of the track.

      If CanControl is false, attempting to call this method should
      have no effect and raise an error.
  -->
    <method name="Stop"/>

  <!--
      Play:

      Starts or resumes playback.

      If already playing, this has no effect.

      If paused, playback resumes from the current position.

      If there is no track to play, this has no effect.

      If CanPlay is false, attempting to call this method should have
      no effect.
  -->
    <method name="Play"/>

  <!--
      Seek:
      @Offset: The number of microseconds to seek forward.

      Seeks forward in the current track by the specified number of
      microseconds.

      A negative value seeks back. If this would mean seeking back
      further than the start of the track, the position is set to 0.

      If the value passed in would mean seeking beyond the end of the
      track, acts like a call to Next.

      If the CanSeek property is false, this has no effect.
  -->
    <method name="Seek">
      <arg type="x" direction="in" name="Offset"/>
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="Time_In_Us"/ -->
    </method>

  <!--
      SetPosition:
      @TrackId: The currently playing track's identifier.
                If this does not match the id of the currently-playing
                track, the call is ignored as "stale".
                /org/mpris/MediaPlayer2/TrackList/NoTrack is not a
                valid value for this argument.
      @Position: Track position in microseconds.
                 This must be between 0 and <track_length>.

      Seeks forward in the current track by the specified number of
      microseconds.

      A negative value seeks back. If this would mean seeking back
      further than the start of the track, the position is set to 0.

      If the value passed in would mean seeking beyond the end of the
      track, acts like a call to Next.

      If the CanSeek property is false, this has no effect.
  -->
    <method name="SetPosition">
      <arg type="o" direction="in" name="TrackId"/>
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="Track_Id"/ -->
      <arg type="x" direction="in" name="Position"/>
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="Time_In_Us"/ -->
    </method>

  <!--
      OpenUri:
      @TrackId: Uri of the track to load. Its uri scheme should be an
                element of the
                org.mpris.MediaPlayer2.SupportedUriSchemes property
                and the mime-type should match one of the elements of
                the org.mpris.MediaPlayer2.SupportedMimeTypes.

      Opens the Uri given as an argument

      If the playback is stopped, starts playing

      If the uri scheme or the mime-type of the uri to open is not
      supported, this method does nothing and may raise an error. In
      particular, if the list of available uri schemes is empty, this
      method may not be implemented.

      Clients should not assume that the Uri has been opened as soon
      as this method returns. They should wait until the mpris:trackid
      field in the Metadata property changes.

      If the media player implements the TrackList interface, then the
      opened track should be made part of the tracklist, the
      org.mpris.MediaPlayer2.TrackList.TrackAdded or
      org.mpris.MediaPlayer2.TrackList.TrackListReplaced signal should
      be fired, as well as the
      org.freedesktop.DBus.Properties.PropertiesChanged signal on the
      tracklist interface.
  -->
    <method name="OpenUri">
      <arg type="s" direction="in" name="Uri"/>
      <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QUrl"/>
    </method>

  <!--
      Seeked:
      @Position: The new position, in microseconds.

      Indicates that the track position has changed in a way that is
      inconsistant with the current playing state.

      When this signal is not received, clients should assume that:
       * When playing, the position progresses according to the rate
         property.
       * When paused, it remains constant.

      This signal does not need to be emitted when playback starts or
      when the track changes, unless the track is starting at an
      unexpected position. An expected position would be the last
      known one when going from Paused to Playing, and 0 when going
      from Stopped to Playing.
  -->
    <signal name="Seeked">
      <arg type="x"  direction="out" name="Position"/>
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="Time_In_Us"/ -->
    </signal>

  <!--
      PlaybackStatus:

      The current playback status.

      May be "Playing", "Paused" or "Stopped".

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="PlaybackStatus" type="s" access="read">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Playback_Status"/ -->
    </property>

  <!--
      LoopStatus:

      The current loop / repeat status

      May be:
       * "None" if the playback will stop when there are no more
         tracks to play
       * "Track" if the current track will start again from the
         begining once it has finished playing
       * "Playlist" if the playback loops through a list of tracks

      If CanControl is false, attempting to set this property should
      have no effect and raise an error.

      This property is optional. Clients should handle its absence
      gracefully.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="LoopStatus" type="s" access="readwrite">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Loop_Status"/ -->
    </property>

  <!--
      Rate:

      The current playback rate.

      The value must fall in the range described by MinimumRate and
      MaximumRate, and must not be 0.0. If playback is paused, the
      PlaybackStatus property should be used to indicate this. A value
      of 0.0 should not be set by the client. If it is, the media
      player should act as though Pause was called.

      If the media player has no ability to play at speeds other than
      the normal playback rate, this must still be implemented, and
      must return 1.0. The MinimumRate and MaximumRate properties must
      also be set to 1.0.

      Not all values may be accepted by the media player. It is left
      to media player implementations to decide how to deal with
      values they cannot use; they may either ignore them or pick a
      "best fit" value. Clients are recommended to only use sensible
      fractions or multiples of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc).

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="Rate" type="d" access="readwrite">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Playback_Rate"/ -->
    </property>

  <!--
      Shuffle:

      A value of false indicates that playback is progressing linearly
      through a playlist, while true means playback is progressing
      through a playlist in some other order.

      If CanControl is false, attempting to set this property should
      have no effect and raise an error.

      This property is optional. Clients should handle its absence
      gracefully.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="Shuffle" type="b" access="readwrite"/>

  <!--
      Metadata:

      The metadata of the current element.

      If there is a current track, this must have a "mpris:trackid"
      entry (of D-Bus type "o") at the very least, which contains a
      D-Bus path that uniquely identifies this track.

      See the type documentation for more details.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="Metadata" type="a{sv}" access="read">
      <annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/>
    </property>

  <!--
      Volume:

      The volume level.

      When setting, if a negative value is passed, the volume should
      be set to 0.0.

      If CanControl is false, attempting to set this property should
      have no effect and raise an error.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="Volume" type="d" access="readwrite">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Volume"/ -->
    </property>

  <!--
      Position:

      The current track position in microseconds, between 0 and the
      'mpris:length' metadata entry (see Metadata).

      Note: If the media player allows it, the current playback
      position can be changed either the SetPosition method or the
      Seek method on this interface.If this is not the case, the
      CanSeek property is false, and setting this property has no
      effect and can raise an error.

      If the playback progresses in a way that is inconstistant with
      the Rate property, the Seeked signal is emited.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is not
      emitted with the new value.
  -->
    <property name="Position" type="x" access="read">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Time_In_Us"/ -->
      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
    </property>

  <!--
      MinimumRate:

      The minimum value which the Rate property can take. Clients
      should not attempt to set the Rate property below this value.

      Note that even if this value is 0.0 or negative, clients should
      not attempt to set the Rate property to 0.0.

      This value should always be 1.0 or less.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="MinimumRate" type="d" access="read">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Playback_Rate"/ -->
    </property>

  <!--
      MaximumRate:

      The maximum value which the Rate property can take. Clients
      should not attempt to set the Rate property above this value.

      This value should always be 1.0 or greater.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="MaximumRate" type="d" access="read">
      <!-- annotation name="org.qtproject.QtDBus.QtTypeName" value="Playback_Rate"/ -->
    </property>

  <!--
      CanGoNext:

      Whether the client can call the Next method on this interface
      and expect the current track to change.

      If it is unknown whether a call to Next will be successful (for
      example, when streaming tracks), this property should be set to
      true.

      If CanControl is false, this property should also be false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="CanGoNext" type="b" access="read"/>

  <!--
      CanGoPrevious:

      Whether the client can call the Previous method on this
      interface and expect the current track to change.

      If it is unknown whether a call to Previous will be successful
      (for example, when streaming tracks), this property should be
      set to true.

      If CanControl is false, this property should also be false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="CanGoPrevious" type="b" access="read"/>

  <!--
      CanPlay:

      Whether playback can be started using Play or PlayPause.

      Note that this is related to whether there is a "current track":
      the value should not depend on whether the track is currently
      paused or playing. In fact, if a track is currently playing (and
      CanControl is true), this should be true.

      If CanControl is false, this property should also be false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="CanPlay" type="b" access="read"/>

  <!--
      CanPause:

      Whether playback can be paused using Pause or PlayPause.

      Note that this is an intrinsic property of the current track:
      its value should not depend on whether the track is currently
      paused or playing. In fact, if playback is currently paused (and
      CanControl is true), this should be true.

      If CanControl is false, this property should also be false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="CanPause" type="b" access="read"/>

  <!--
      CanSeek:

      Whether the client can control the playback position using Seek
      and SetPosition. This may be different for different tracks.

      If CanControl is false, this property should also be false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is
      emitted with the new value.
  -->
    <property name="CanSeek" type="b" access="read"/>

  <!--
      CanControl:

      Whether the media player may be controlled over this interface.

      This property is not expected to change, as it describes an
      intrinsic capability of the implementation.

      If this is false, clients should assume that all properties on
      this interface are read-only (and will raise errors if writing
      to them is attempted), no methods are implemented and all other
      properties starting with "Can" are also false.

      When this property changes, the
      org.freedesktop.DBus.Properties.PropertiesChanged signal is not
      emitted with the new value.
  -->
    <property name="CanControl" type="b" access="read">
      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
    </property>

  </interface>
</node>


M src/app.hpp => src/app.hpp +9 -3
@@ 14,6 14,7 @@
#include "genre.hpp"
#include "ini.hpp"
#include "model.hpp"
#include "mpris-controller.hpp"
#include "player.hpp"
#include "prefs.hpp"
#include "service.hpp"


@@ 41,6 42,9 @@ struct SomaFM
        _app = Gtk::Application::create(argc, argv, "com.zanneth.somafmgtk");
        _builder = Gtk::Builder::create_from_string(__mainui); assert(_builder);

        DBus::Connection conn = DBus::Connection::SessionBus();
        _mpris_controller = std::make_unique<MPRISController>(conn, _player);

        _setup_widgets();
        _setup_list_model();
        _connect_signals();


@@ 169,9 173,9 @@ private:
        _volume_button->signal_value_changed().connect(sigc::mem_fun(*this, &SomaFM::_on_volume_value_changed));
        _reload_dispatcher.connect(sigc::mem_fun(*this, &SomaFM::_on_reload_finished));
        _error_dispatcher.connect(sigc::mem_fun(*this, &SomaFM::_on_error_dispatch));
        _player.set_playback_state_changed_callback(std::bind(&SomaFM::_on_playback_state_changed, this, std::placeholders::_1));
        _player.set_channel_changed_callback(std::bind(&SomaFM::_on_channel_changed, this, std::placeholders::_1));
        _player.set_error_callback(std::bind(&SomaFM::_show_error, this, std::placeholders::_1));
        _player.on_playback_state_changed.connect(sigc::mem_fun(*this, &SomaFM::_on_playback_state_changed));
        _player.on_channel_changed.connect(sigc::mem_fun(*this, &SomaFM::_on_channel_changed));
        _player.on_error.connect(sigc::mem_fun(*this, &SomaFM::_show_error));
        _tree_artwork_queue.set_completion_callback(std::bind(&SomaFM::_on_tree_artwork_completed, this, std::placeholders::_1));
        _now_playing_artwork_queue.set_completion_callback(std::bind(&SomaFM::_on_now_playing_artwork_completed, this, std::placeholders::_1));
        _list_model->signal_sort_column_changed().connect(sigc::mem_fun(*this, &SomaFM::_on_stations_sort_column_changed));


@@ 537,4 541,6 @@ private:
    std::stack<std::string>             _pending_errors;
    bool                                _pending_channels_reload;
    bool                                _pending_songs_reload;

    std::unique_ptr<MPRISController>    _mpris_controller;
};

M src/main.cpp => src/main.cpp +6 -0
@@ 1,11 1,17 @@
#include <gstreamermm.h>
#include <dbus-c++/glib-integration.h>
#include "app.hpp"

int
main(int argc, char **argv)
{
    DBus::Glib::BusDispatcher dispatcher;
    DBus::default_dispatcher = &dispatcher;

    Gst::init(argc, argv);

    SomaFM app(argc, argv);
    dispatcher.attach(g_main_context_default());

    return app.run();
}

A src/mpris-controller.hpp => src/mpris-controller.hpp +82 -0
@@ 0,0 1,82 @@
#pragma once 

#include "player.hpp"
#include "../generated/mpris_adaptor.h"

struct MPRISController : 
    public org::mpris::MediaPlayer2::Player_adaptor, 
    public DBus::ObjectAdaptor, DBus::IntrospectableAdaptor
{
    MPRISController(DBus::Connection &connection, Player &player) : 
        org::mpris::MediaPlayer2::Player_adaptor(), 
        DBus::ObjectAdaptor(connection, "/org/mpris/MediaPlayer2"), 
        _player(player)
    {
        // Register the service
        connection.request_name("org.mpris.MediaPlayer2.somafm");

        // Update capabilities
        CanGoNext = false;
        CanGoPrevious = false;
        CanSeek = false;
        PlaybackStatus = "Stopped";

        // Signals 
        _player.on_playback_state_changed.connect(
            sigc::mem_fun(*this, &MPRISController::_on_playback_state_changed)
        );
    }

    void 
    Pause() override 
    {
        if (_player.is_playing()) {
            _player.pause();
        }
    } 

    void 
    PlayPause() override 
    {
        if (_player.is_playing()) {
            _player.pause();
        } else {
            _player.play();
        }
    }

    void 
    Stop() override 
    {
        Pause();
    } 

    void 
    Play() override 
    {
        if (!_player.is_playing()) {
            _player.play();
        }
    }

    void
    _on_playback_state_changed(bool is_playing)
    {
        if (is_playing) {
            PlaybackStatus = "Playing";
        } else {
            PlaybackStatus = "Paused";
        }
    }

    // Unsupported methods

    void Next() override {} 
    void Previous() override {} 
    void Seek(const int64_t& Offset) override {} 
    void SetPosition(const ::DBus::Path& TrackId, const int64_t& Position) override {}
    void OpenUri(const std::string &uri) override {} 

private: 
    Player &_player;
};

M src/player.hpp => src/player.hpp +6 -33
@@ 9,9 9,9 @@

struct Player
{
    using PlaybackStateChangedCb = std::function<void(bool)>;
    using ChannelChangedCb = std::function<void(const std::optional<Channel> &)>;
    using ErrorCb = std::function<void(const std::string &)>;
    sigc::signal<void, const std::string &>            on_error;
    sigc::signal<void, const std::optional<Channel> &> on_channel_changed;
    sigc::signal<void, bool>                           on_playback_state_changed;

    Player() :
        _playbin(nullptr)


@@ 63,24 63,6 @@ struct Player
        }
    }

    void
    set_playback_state_changed_callback(const PlaybackStateChangedCb &cb)
    {
        _playback_state_changed_cb = cb;
    }

    void
    set_channel_changed_callback(const ChannelChangedCb &cb)
    {
        _channel_changed_cb = cb;
    }

    void
    set_error_callback(const ErrorCb &cb)
    {
        _error_cb = cb;
    }

    std::optional<Channel>
    channel()
    {


@@ 99,9 81,7 @@ struct Player
            _channel = channel;
            _cached_file_url = std::nullopt;

            if (_channel_changed_cb) {
                _channel_changed_cb(_channel);
            }
            on_channel_changed.emit(_channel);
        }

        auto playlist = channel.highest_quality_playlist(Format::MP3);


@@ 184,12 164,10 @@ private:
    bool
    _on_bus_message(const Glib::RefPtr<Gst::Bus> &, const Glib::RefPtr<Gst::Message> &msg)
    {
        if (!_playback_state_changed_cb) return true;

        if (msg->get_message_type() == Gst::MESSAGE_STATE_CHANGED) {
            auto elm = Glib::RefPtr<Gst::Element>::cast_dynamic(msg->get_source());
            if (elm == _playbin) {
                _playback_state_changed_cb(is_playing());
                on_playback_state_changed.emit(is_playing());
            }
        }



@@ 199,9 177,7 @@ private:
    void
    _report_error(const std::string &err)
    {
        if (_error_cb) {
            _error_cb(err);
        }
        on_error.emit(err);
    }

private:


@@ 209,9 185,6 @@ private:
    Glib::RefPtr<Gst::Element> _playbin;
    std::optional<Channel>     _channel;
    std::optional<std::string> _cached_file_url;
    PlaybackStateChangedCb     _playback_state_changed_cb;
    ChannelChangedCb           _channel_changed_cb;
    ErrorCb                    _error_cb;
};

// -----------------------------------------------------------------------------