|
| 1 | +#include <algorithm> |
| 2 | +#include <atomic> |
| 3 | +#include <optional> |
| 4 | +#include <string> |
| 5 | +#include <thread> |
| 6 | +#include <unordered_map> |
| 7 | +#include <ranges> |
| 8 | +#include <gio/gio.h> |
1 | 9 | #include <os/media.h> |
| 10 | +#include <os/logger.h> |
| 11 | + |
| 12 | +enum class PlaybackStatus |
| 13 | +{ |
| 14 | + Stopped, |
| 15 | + Playing, |
| 16 | + Paused |
| 17 | +}; |
| 18 | + |
| 19 | +static const char* DBusInterface = "org.freedesktop.DBus"; |
| 20 | +static const char* DBusPropertiesInterface = "org.freedesktop.DBus.Properties"; |
| 21 | +static const char* DBusPath = "/org/freedesktop/DBus"; |
| 22 | +static const char* MPRIS2Interface = "org.mpris.MediaPlayer2"; |
| 23 | +static const char* MPRIS2PlayerInterface = "org.mpris.MediaPlayer2.Player"; |
| 24 | +static const char* MPRIS2Path = "/org/mpris/MediaPlayer2"; |
| 25 | + |
| 26 | +static std::optional<std::thread> g_dbusThread; |
| 27 | +static std::unordered_map<std::string, PlaybackStatus> g_playerStatus; |
| 28 | +static std::atomic<bool> g_isPlaying = false; |
| 29 | + |
| 30 | +static PlaybackStatus PlaybackStatusFromString(const char* str) |
| 31 | +{ |
| 32 | + if (g_str_equal(str, "Playing")) |
| 33 | + return PlaybackStatus::Playing; |
| 34 | + else if (g_str_equal(str, "Paused")) |
| 35 | + return PlaybackStatus::Paused; |
| 36 | + else |
| 37 | + return PlaybackStatus::Stopped; |
| 38 | +} |
| 39 | + |
| 40 | +static void UpdateActiveStatus() |
| 41 | +{ |
| 42 | + g_isPlaying = std::ranges::any_of( |
| 43 | + g_playerStatus | std::views::values, |
| 44 | + [](PlaybackStatus status) { return status == PlaybackStatus::Playing; } |
| 45 | + ); |
| 46 | +} |
| 47 | + |
| 48 | +static void UpdateActivePlayers(const char* name, PlaybackStatus status) |
| 49 | +{ |
| 50 | + g_playerStatus.insert_or_assign(name, status); |
| 51 | + UpdateActiveStatus(); |
| 52 | +} |
| 53 | + |
| 54 | +static PlaybackStatus MPRISGetPlaybackStatus(GDBusConnection* connection, const gchar* name) |
| 55 | +{ |
| 56 | + GError* error; |
| 57 | + GVariant* response; |
| 58 | + GVariant* tupleChild; |
| 59 | + GVariant* value; |
| 60 | + PlaybackStatus status; |
| 61 | + |
| 62 | + error = NULL; |
| 63 | + |
| 64 | + response = g_dbus_connection_call_sync( |
| 65 | + connection, |
| 66 | + name, |
| 67 | + MPRIS2Path, |
| 68 | + DBusPropertiesInterface, |
| 69 | + "Get", |
| 70 | + g_variant_new("(ss)", MPRIS2PlayerInterface, "PlaybackStatus"), |
| 71 | + G_VARIANT_TYPE("(v)"), |
| 72 | + G_DBUS_CALL_FLAGS_NONE, |
| 73 | + -1, |
| 74 | + NULL, |
| 75 | + &error |
| 76 | + ); |
| 77 | + |
| 78 | + if (!response) |
| 79 | + { |
| 80 | + LOGF_ERROR("Failed to process D-Bus Get: {}", error->message); |
| 81 | + g_clear_error(&error); |
| 82 | + return PlaybackStatus::Stopped; |
| 83 | + } |
| 84 | + |
| 85 | + tupleChild = g_variant_get_child_value(response, 0); |
| 86 | + value = g_variant_get_variant(tupleChild); |
| 87 | + |
| 88 | + if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) |
| 89 | + { |
| 90 | + LOG_ERROR("Failed to process D-Bus Get"); |
| 91 | + g_variant_unref(tupleChild); |
| 92 | + return PlaybackStatus::Stopped; |
| 93 | + } |
| 94 | + |
| 95 | + status = PlaybackStatusFromString(g_variant_get_string(value, NULL)); |
| 96 | + |
| 97 | + g_variant_unref(value); |
| 98 | + g_variant_unref(tupleChild); |
| 99 | + g_variant_unref(response); |
| 100 | + |
| 101 | + return status; |
| 102 | +} |
| 103 | + |
| 104 | +// Something is very wrong with the system if this happens |
| 105 | +static void DBusConnectionClosed(GDBusConnection* connection, |
| 106 | + gboolean remotePeerVanished, |
| 107 | + GError* error, |
| 108 | + gpointer userData) |
| 109 | +{ |
| 110 | + LOG_ERROR("D-Bus connection closed"); |
| 111 | + g_isPlaying = false; |
| 112 | + g_main_loop_quit((GMainLoop*)userData); |
| 113 | +} |
| 114 | + |
| 115 | +static void DBusNameOwnerChanged(GDBusConnection* connection, |
| 116 | + const gchar* senderName, |
| 117 | + const gchar* objectPath, |
| 118 | + const gchar* interfaceName, |
| 119 | + const gchar* signalName, |
| 120 | + GVariant* parameters, |
| 121 | + gpointer userData) |
| 122 | +{ |
| 123 | + const char* name; |
| 124 | + const char* oldOwner; |
| 125 | + const char* newOwner; |
| 126 | + |
| 127 | + g_variant_get(parameters, "(&s&s&s)", &name, &oldOwner, &newOwner); |
| 128 | + |
| 129 | + if (g_str_has_prefix(name, MPRIS2Interface)) |
| 130 | + { |
| 131 | + if (oldOwner[0]) |
| 132 | + { |
| 133 | + g_playerStatus.erase(oldOwner); |
| 134 | + } |
| 135 | + |
| 136 | + UpdateActiveStatus(); |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +static void MPRISPropertiesChanged(GDBusConnection* connection, |
| 141 | + const gchar* senderName, |
| 142 | + const gchar* objectPath, |
| 143 | + const gchar* interfaceName, |
| 144 | + const gchar* signalName, |
| 145 | + GVariant* parameters, |
| 146 | + gpointer userData) |
| 147 | +{ |
| 148 | + const char* interface; |
| 149 | + GVariant* changed; |
| 150 | + GVariantIter iter; |
| 151 | + const char* key; |
| 152 | + GVariant* value; |
| 153 | + PlaybackStatus playbackStatus; |
| 154 | + |
| 155 | + g_variant_get_child(parameters, 0, "&s", &interface); |
| 156 | + g_variant_get_child(parameters, 1, "@a{sv}", &changed); |
| 157 | + |
| 158 | + g_variant_iter_init(&iter, changed); |
| 159 | + while (g_variant_iter_next(&iter, "{&sv}", &key, &value)) |
| 160 | + { |
| 161 | + if (g_str_equal(key, "PlaybackStatus")) |
| 162 | + { |
| 163 | + playbackStatus = PlaybackStatusFromString(g_variant_get_string(value, NULL)); |
| 164 | + UpdateActivePlayers(senderName, playbackStatus); |
| 165 | + g_variant_unref(value); |
| 166 | + break; |
| 167 | + } |
| 168 | + g_variant_unref(value); |
| 169 | + } |
| 170 | + |
| 171 | + g_variant_unref(changed); |
| 172 | +} |
| 173 | + |
| 174 | +/* Called upon CONNECT to discover already active MPRIS2 players by looking for |
| 175 | + well-known bus names that begin with the MPRIS2 path. |
| 176 | + g_playerStatus stores unique connection names, |
| 177 | + not their well-known ones, as the PropertiesChanged signal only provides the |
| 178 | + former. */ |
| 179 | +static void DBusListNamesReceived(GObject* object, GAsyncResult* res, gpointer userData) |
| 180 | +{ |
| 181 | + GDBusConnection* connection; |
| 182 | + GError* error; |
| 183 | + GVariant* response; |
| 184 | + GVariant* tupleChild; |
| 185 | + GVariantIter iter; |
| 186 | + const gchar* name; |
| 187 | + |
| 188 | + connection = G_DBUS_CONNECTION(object); |
| 189 | + error = NULL; |
| 190 | + response = g_dbus_connection_call_finish(connection, res, &error); |
| 191 | + |
| 192 | + if (!response) |
| 193 | + { |
| 194 | + LOGF_ERROR("Failed to process D-Bus ListNames: {}", error->message); |
| 195 | + g_clear_error(&error); |
| 196 | + return; |
| 197 | + } |
| 198 | + |
| 199 | + tupleChild = g_variant_get_child_value(response, 0); |
| 200 | + |
| 201 | + g_variant_iter_init(&iter, tupleChild); |
| 202 | + while (g_variant_iter_next(&iter, "&s", &name)) |
| 203 | + { |
| 204 | + GVariant* ownerResponse; |
| 205 | + const gchar* ownerName; |
| 206 | + PlaybackStatus status; |
| 207 | + |
| 208 | + if (!g_str_has_prefix(name, MPRIS2Interface)) |
| 209 | + continue; |
| 210 | + |
| 211 | + ownerResponse = g_dbus_connection_call_sync( |
| 212 | + connection, |
| 213 | + DBusInterface, |
| 214 | + DBusPath, |
| 215 | + DBusInterface, |
| 216 | + "GetNameOwner", |
| 217 | + g_variant_new("(s)", name), |
| 218 | + G_VARIANT_TYPE("(s)"), |
| 219 | + G_DBUS_CALL_FLAGS_NONE, |
| 220 | + -1, |
| 221 | + NULL, |
| 222 | + &error |
| 223 | + ); |
| 224 | + |
| 225 | + if (!ownerResponse) |
| 226 | + { |
| 227 | + LOGF_ERROR("Failed to process D-Bus GetNameOwner: {}", error->message); |
| 228 | + g_clear_error(&error); |
| 229 | + g_variant_unref(tupleChild); |
| 230 | + g_variant_unref(response); |
| 231 | + return; |
| 232 | + } |
| 233 | + |
| 234 | + g_variant_get(ownerResponse, "(&s)", &ownerName); |
| 235 | + status = MPRISGetPlaybackStatus(connection, ownerName); |
| 236 | + |
| 237 | + g_playerStatus.insert_or_assign(ownerName, status); |
| 238 | + g_variant_unref(ownerResponse); |
| 239 | + } |
| 240 | + |
| 241 | + UpdateActiveStatus(); |
| 242 | + |
| 243 | + g_variant_unref(tupleChild); |
| 244 | + g_variant_unref(response); |
| 245 | +} |
| 246 | + |
| 247 | +static void DBusThreadProc() |
| 248 | +{ |
| 249 | + GMainContext* mainContext; |
| 250 | + GMainLoop* mainLoop; |
| 251 | + GError* error; |
| 252 | + GDBusConnection* connection; |
| 253 | + |
| 254 | + mainContext = g_main_context_new(); |
| 255 | + g_main_context_push_thread_default(mainContext); |
| 256 | + mainLoop = g_main_loop_new(mainContext, FALSE); |
| 257 | + error = NULL; |
| 258 | + |
| 259 | + connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); |
| 260 | + if (!connection) |
| 261 | + { |
| 262 | + LOGF_ERROR("Failed to connect to D-Bus: {}", error->message); |
| 263 | + g_clear_error(&error); |
| 264 | + g_main_context_unref(mainContext); |
| 265 | + g_main_loop_unref(mainLoop); |
| 266 | + return; |
| 267 | + } |
| 268 | + |
| 269 | + g_dbus_connection_set_exit_on_close(connection, FALSE); |
| 270 | + g_signal_connect(connection, "closed", G_CALLBACK(DBusConnectionClosed), mainLoop); |
| 271 | + |
| 272 | + // Listen for player connection changes |
| 273 | + g_dbus_connection_signal_subscribe( |
| 274 | + connection, |
| 275 | + DBusInterface, |
| 276 | + DBusInterface, |
| 277 | + "NameOwnerChanged", |
| 278 | + DBusPath, |
| 279 | + NULL, |
| 280 | + G_DBUS_SIGNAL_FLAGS_NONE, |
| 281 | + DBusNameOwnerChanged, |
| 282 | + NULL, |
| 283 | + NULL |
| 284 | + ); |
| 285 | + |
| 286 | + // Listen for player status changes |
| 287 | + g_dbus_connection_signal_subscribe( |
| 288 | + connection, |
| 289 | + NULL, |
| 290 | + DBusPropertiesInterface, |
| 291 | + "PropertiesChanged", |
| 292 | + MPRIS2Path, |
| 293 | + NULL, |
| 294 | + G_DBUS_SIGNAL_FLAGS_NONE, |
| 295 | + MPRISPropertiesChanged, |
| 296 | + NULL, |
| 297 | + NULL |
| 298 | + ); |
| 299 | + |
| 300 | + // Request list of current players |
| 301 | + g_dbus_connection_call( |
| 302 | + connection, |
| 303 | + DBusInterface, |
| 304 | + DBusPath, |
| 305 | + DBusInterface, |
| 306 | + "ListNames", |
| 307 | + NULL, |
| 308 | + G_VARIANT_TYPE("(as)"), |
| 309 | + G_DBUS_CALL_FLAGS_NONE, |
| 310 | + -1, |
| 311 | + NULL, |
| 312 | + DBusListNamesReceived, |
| 313 | + NULL |
| 314 | + ); |
| 315 | + |
| 316 | + g_main_loop_run(mainLoop); |
| 317 | +} |
2 | 318 |
|
3 | 319 | bool os::media::IsExternalMediaPlaying() |
4 | 320 | { |
5 | | - // This functionality is not supported in Linux. |
6 | | - return false; |
| 321 | + if (!g_dbusThread) |
| 322 | + { |
| 323 | + g_dbusThread.emplace(DBusThreadProc); |
| 324 | + g_dbusThread->detach(); |
| 325 | + } |
| 326 | + |
| 327 | + return g_isPlaying; |
7 | 328 | } |
0 commit comments