initial MPRIS support

This commit is contained in:
Enno Boland 2024-09-01 19:29:22 +02:00
parent 21e2e2606e
commit 4977061abd
7 changed files with 447 additions and 0 deletions

View file

@ -101,6 +101,11 @@ if usb_support
]
endif
mpris_support = get_option('mpris') and host_machine.system() == 'linux'
if mpris_support
src += [ 'src/mpris.c' ]
endif
cc = meson.get_compiler('c')
dependencies = [
@ -119,6 +124,11 @@ if usb_support
dependencies += dependency('libusb-1.0')
endif
if mpris_support
dependencies += dependency('glib-2.0')
dependencies += dependency('gio-2.0')
endif
if host_machine.system() == 'windows'
dependencies += cc.find_library('mingw32')
dependencies += cc.find_library('ws2_32')
@ -170,6 +180,9 @@ conf.set('HAVE_V4L2', v4l2_support)
# enable HID over AOA support (linux only)
conf.set('HAVE_USB', usb_support)
# enable DBus MPRIS support (linux only)
conf.set('HAVE_MPRIS', mpris_support)
configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src')

View file

@ -8,3 +8,4 @@
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)
#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9)
#define SC_EVENT_RAISE_WINDOW (SDL_USEREVENT + 10)

377
app/src/mpris.c Normal file
View file

@ -0,0 +1,377 @@
#include "mpris.h"
#include "events.h"
#include <SDL_events.h>
#include "util/log.h"
#include <gio/gio.h>
#include <glib-unix.h>
#include <string.h>
static const char *introspection_xml =
"<node>\n"
" <interface name=\"org.mpris.MediaPlayer2\">\n"
" <method name=\"Raise\"/>\n"
" <method name=\"Quit\"/>\n"
" <property name=\"CanQuit\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanSetFullscreen\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanRaise\" type=\"b\" access=\"read\"/>\n"
" <property name=\"Fullscreen\" type=\"b\" access=\"readwrite\"/>\n"
" <property name=\"HasTrackList\" type=\"b\" access=\"read\"/>\n"
" <property name=\"Identity\" type=\"s\" access=\"read\"/>\n"
" <property name=\"DesktopEntry\" type=\"s\" access=\"read\"/>\n"
" <property name=\"SupportedUriSchemes\" type=\"as\" access=\"read\"/>\n"
" <property name=\"SupportedMimeTypes\" type=\"as\" access=\"read\"/>\n"
" </interface>\n"
" <interface name=\"org.mpris.MediaPlayer2.Player\">\n"
" <method name=\"Next\" />\n"
" <method name=\"Previous\" />\n"
" <method name=\"Pause\" />\n"
" <method name=\"PlayPause\" />\n"
" <method name=\"Stop\" />\n"
" <method name=\"Play\" />\n"
" <method name=\"Seek\">\n"
" <arg type=\"x\" name=\"Offset\" direction=\"in\"/>\n"
" </method>\n"
" <method name=\"SetPosition\">\n"
" <arg type=\"o\" name=\"TrackId\" direction=\"in\"/>\n"
" <arg type=\"x\" name=\"Offset\" direction=\"in\"/>\n"
" </method>\n"
" <method name=\"OpenUri\">\n"
" <arg type=\"s\" name=\"Uri\" direction=\"in\"/>\n"
" </method>\n"
" <signal name=\"Seeked\">\n"
" <arg type=\"x\" name=\"Position\" direction=\"out\"/>\n"
" </signal>\n"
" <property name=\"PlaybackStatus\" type=\"s\" access=\"read\"/>\n"
" <property name=\"LoopStatus\" type=\"s\" access=\"readwrite\"/>\n"
" <property name=\"Rate\" type=\"d\" access=\"readwrite\"/>\n"
" <property name=\"Shuffle\" type=\"b\" access=\"readwrite\"/>\n"
" <property name=\"Metadata\" type=\"a{sv}\" access=\"read\"/>\n"
" <property name=\"Volume\" type=\"d\" access=\"readwrite\"/>\n"
" <property name=\"Position\" type=\"x\" access=\"read\"/>\n"
" <property name=\"MinimumRate\" type=\"d\" access=\"read\"/>\n"
" <property name=\"MaximumRate\" type=\"d\" access=\"read\"/>\n"
" <property name=\"CanGoNext\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanGoPrevious\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanPlay\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanPause\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanSeek\" type=\"b\" access=\"read\"/>\n"
" <property name=\"CanControl\" type=\"b\" access=\"read\"/>\n"
" </interface>\n"
"</node>\n";
static inline void
push_event(uint32_t type, const char *name) {
SDL_Event event;
event.type = type;
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGE("Could not post %s event: %s", name, SDL_GetError());
// What could we do?
}
}
#define PUSH_EVENT(TYPE) push_event(TYPE, #TYPE)
static void
method_call_root(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *object_path,
G_GNUC_UNUSED const char *interface_name,
const char *method_name, G_GNUC_UNUSED GVariant *parameters,
GDBusMethodInvocation *invocation, G_GNUC_UNUSED gpointer user_data) {
if (g_strcmp0(method_name, "Quit") == 0) {
LOGD("mpris: quit");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Raise") == 0) {
LOGD("mpris: raise window");
PUSH_EVENT(SC_EVENT_RAISE_WINDOW);
g_dbus_method_invocation_return_value(invocation, NULL);
} else {
LOGW("mpris: unknown method %s", method_name);
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_UNKNOWN_METHOD,
"Unknown method");
}
}
static GVariant *
get_property_root(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *object_path,
G_GNUC_UNUSED const char *interface_name,
const char *property_name, G_GNUC_UNUSED GError **error,
G_GNUC_UNUSED gpointer user_data) {
GVariant *ret;
printf("Getting property %s\n", property_name);
if (g_strcmp0(property_name, "CanQuit") == 0) {
ret = g_variant_new_boolean(FALSE);
} else if (g_strcmp0(property_name, "Fullscreen") == 0) {
int fullscreen = 0;
ret = g_variant_new_boolean(fullscreen);
} else if (g_strcmp0(property_name, "CanSetFullscreen") == 0) {
ret = g_variant_new_boolean(FALSE);
} else if (g_strcmp0(property_name, "CanRaise") == 0) {
ret = g_variant_new_boolean(TRUE);
} else if (g_strcmp0(property_name, "HasTrackList") == 0) {
ret = g_variant_new_boolean(FALSE);
} else if (g_strcmp0(property_name, "Identity") == 0) {
ret = g_variant_new_string("Identity");
} else if (g_strcmp0(property_name, "DesktopEntry") == 0) {
ret = g_variant_new_string("scrcpy");
} else if (g_strcmp0(property_name, "SupportedUriSchemes") == 0) {
ret = NULL;
} else if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) {
ret = NULL;
} else {
ret = NULL;
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY,
"Unknown property %s", property_name);
}
return ret;
}
static gboolean
set_property_root(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *object_path,
G_GNUC_UNUSED const char *interface_name,
const char *property_name, GVariant *value,
G_GNUC_UNUSED GError **error, G_GNUC_UNUSED gpointer user_data) {
if (g_strcmp0(property_name, "Fullscreen") == 0) {
int fullscreen;
g_variant_get(value, "b", &fullscreen);
LOGD("mpris: setting fullscreen to %d", fullscreen);
} else {
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY,
"Cannot set property %s", property_name);
return FALSE;
}
return TRUE;
}
static GDBusInterfaceVTable vtable_root = {
method_call_root, get_property_root, set_property_root, {0}};
static void
method_call_player(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *_object_path,
G_GNUC_UNUSED const char *interface_name,
const char *method_name, G_GNUC_UNUSED GVariant *parameters,
GDBusMethodInvocation *invocation, G_GNUC_UNUSED gpointer user_data) {
if (g_strcmp0(method_name, "Pause") == 0) {
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "PlayPause") == 0) {
LOGD("mpris: PlayPause");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Play") == 0) {
LOGD("mpris: Play");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Stop") == 0) {
LOGD("mpris: Stop");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Next") == 0) {
LOGD("mpris: Next");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Previous") == 0) {
LOGD("mpris: Previous");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "Seek") == 0) {
LOGD("mpris: Seek");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "SetPosition") == 0) {
LOGD("mpris: SetPosition");
g_dbus_method_invocation_return_value(invocation, NULL);
} else if (g_strcmp0(method_name, "OpenUri") == 0) {
LOGD("mpris: OpenUri");
g_dbus_method_invocation_return_value(invocation, NULL);
} else {
LOGW("mpris: unknown method %s", method_name);
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_UNKNOWN_METHOD,
"Unknown method");
}
}
static GVariant *
get_property_player(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *object_path,
G_GNUC_UNUSED const char *interface_name,
const char *property_name, GError **error,
G_GNUC_UNUSED gpointer user_data) {
GVariant *ret;
if (g_strcmp0(property_name, "PlaybackStatus") == 0) {
ret = g_variant_new_string("Stopped");
} else if (g_strcmp0(property_name, "LoopStatus") == 0) {
ret = g_variant_new_string("None");
} else if (g_strcmp0(property_name, "Rate") == 0) {
ret = g_variant_new_double(1.0);
} else if (g_strcmp0(property_name, "Shuffle") == 0) {
ret = g_variant_new_boolean(FALSE);
} else if (g_strcmp0(property_name, "Metadata") == 0) {
ret = NULL;
} else if (g_strcmp0(property_name, "Volume") == 0) {
ret = g_variant_new_double(100);
} else if (g_strcmp0(property_name, "Position") == 0) {
ret = g_variant_new_int64(0);
} else if (g_strcmp0(property_name, "MinimumRate") == 0) {
ret = g_variant_new_double(100);
} else if (g_strcmp0(property_name, "MaximumRate") == 0) {
ret = g_variant_new_double(100);
} else if (g_strcmp0(property_name, "CanGoNext") == 0) {
ret = g_variant_new_boolean(TRUE);
} else if (g_strcmp0(property_name, "CanGoPrevious") == 0) {
ret = g_variant_new_boolean(TRUE);
} else if (g_strcmp0(property_name, "CanPlay") == 0) {
ret = g_variant_new_boolean(TRUE);
} else if (g_strcmp0(property_name, "CanPause") == 0) {
ret = g_variant_new_boolean(TRUE);
} else if (g_strcmp0(property_name, "CanSeek") == 0) {
ret = g_variant_new_boolean(FALSE);
} else if (g_strcmp0(property_name, "CanControl") == 0) {
ret = g_variant_new_boolean(TRUE);
} else {
ret = NULL;
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY,
"Unknown property %s", property_name);
}
return ret;
}
static gboolean
set_property_player(G_GNUC_UNUSED GDBusConnection *connection,
G_GNUC_UNUSED const char *sender,
G_GNUC_UNUSED const char *object_path,
G_GNUC_UNUSED const char *interface_name,
const char *property_name, GVariant *value,
G_GNUC_UNUSED GError **error, G_GNUC_UNUSED gpointer user_data) {
if (g_strcmp0(property_name, "LoopStatus") == 0) {
LOGD("mpris: setting loop status");
} else if (g_strcmp0(property_name, "Rate") == 0) {
double rate = g_variant_get_double(value);
LOGD("mpris: setting rate to %f", rate);
} else if (g_strcmp0(property_name, "Shuffle") == 0) {
int shuffle = g_variant_get_boolean(value);
LOGD("mpris: setting shuffle to %d", shuffle);
} else if (g_strcmp0(property_name, "Volume") == 0) {
double volume = g_variant_get_double(value);
LOGD("mpris: setting volume to %f", volume);
} else {
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY,
"Cannot set property %s", property_name);
return FALSE;
}
return TRUE;
}
static GDBusInterfaceVTable vtable_player = {
method_call_player, get_property_player, set_property_player, {0}};
static void
on_bus_acquired(GDBusConnection *connection, G_GNUC_UNUSED const char *name,
gpointer user_data) {
GError *error = NULL;
struct sc_mpris *ud = user_data;
ud->connection = connection;
ud->root_interface_id = g_dbus_connection_register_object(
connection, "/org/mpris/MediaPlayer2", ud->root_interface_info,
&vtable_root, user_data, NULL, &error);
if (error != NULL) {
g_printerr("%s", error->message);
}
ud->player_interface_id = g_dbus_connection_register_object(
connection, "/org/mpris/MediaPlayer2", ud->player_interface_info,
&vtable_player, user_data, NULL, &error);
if (error != NULL) {
g_printerr("%s", error->message);
}
}
static void
on_name_lost(GDBusConnection *connection, G_GNUC_UNUSED const char *_name,
gpointer user_data) {
if (connection) {
struct sc_mpris *ud = user_data;
pid_t pid = getpid();
char *name =
g_strdup_printf("org.mpris.MediaPlayer2.scrcpy.instance%d", pid);
ud->bus_id = g_bus_own_name(G_BUS_TYPE_SESSION, name,
G_BUS_NAME_OWNER_FLAGS_NONE, NULL, NULL,
NULL, &ud, NULL);
g_free(name);
}
}
static int
run_mpris(void *data) {
struct sc_mpris *mpris = data;
GMainContext *ctx;
ctx = g_main_context_new();
mpris->loop = g_main_loop_new(ctx, FALSE);
g_main_context_push_thread_default(ctx);
mpris->bus_id =
g_bus_own_name(G_BUS_TYPE_SESSION, "org.mpris.MediaPlayer2.scrcpy",
G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, on_bus_acquired,
NULL, on_name_lost, mpris, NULL);
g_main_context_pop_thread_default(ctx);
g_main_loop_run(mpris->loop);
g_dbus_connection_unregister_object(mpris->connection,
mpris->root_interface_id);
g_dbus_connection_unregister_object(mpris->connection,
mpris->player_interface_id);
g_bus_unown_name(mpris->bus_id);
g_main_loop_unref(mpris->loop);
g_main_context_unref(ctx);
g_dbus_node_info_unref(mpris->introspection_data);
return 0;
}
bool
sc_mpris_start(struct sc_mpris *mpris) {
LOGD("mpris: starting glib thread");
GError *error = NULL;
// Load introspection data and split into separate interfaces
mpris->introspection_data =
g_dbus_node_info_new_for_xml(introspection_xml, &error);
if (error != NULL) {
g_printerr("%s", error->message);
}
mpris->root_interface_info = g_dbus_node_info_lookup_interface(
mpris->introspection_data, "org.mpris.MediaPlayer2");
mpris->player_interface_info = g_dbus_node_info_lookup_interface(
mpris->introspection_data, "org.mpris.MediaPlayer2.Player");
mpris->changed_properties = g_hash_table_new(g_str_hash, g_str_equal);
mpris->metadata = NULL;
bool ok =
sc_thread_create(&mpris->thread, run_mpris, "scrcpy-mpris", mpris);
if (!ok) {
LOGE("mpris: cloud not start mpris thread");
return false;
}
return true;
}
void
sc_mpris_stop(struct sc_mpris *mpris) {
LOGD("mpris: stopping glib thread");
g_main_loop_quit(mpris->loop);
sc_thread_join(&mpris->thread, NULL);
}

29
app/src/mpris.h Normal file
View file

@ -0,0 +1,29 @@
#ifndef SC_MPRIS_H
#define SC_MPRIS_H
#include "common.h"
#include "util/thread.h"
#include <sys/wait.h>
#include <gio/gio.h>
struct sc_mpris {
sc_thread thread;
GMainLoop *loop;
gint bus_id;
GDBusNodeInfo *introspection_data;
GDBusConnection *connection;
GDBusInterfaceInfo *root_interface_info;
GDBusInterfaceInfo *player_interface_info;
guint root_interface_id;
guint player_interface_id;
const char *status;
const char *loop_status;
GHashTable *changed_properties;
GVariant *metadata;
};
bool sc_mpris_start(struct sc_mpris *mpris);
void sc_mpris_stop(struct sc_mpris *mpris);
#endif

View file

@ -41,6 +41,9 @@
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
#ifdef HAVE_MPRIS
# include "mpris.h"
#endif
struct scrcpy {
struct sc_server server;
@ -79,6 +82,9 @@ struct scrcpy {
struct sc_mouse_aoa mouse_aoa;
#endif
};
#ifdef HAVE_MPRIS
struct sc_mpris mpris;
#endif
struct sc_timeout timeout;
};
@ -358,6 +364,9 @@ scrcpy(struct scrcpy_options *options) {
bool aoa_hid_initialized = false;
bool keyboard_aoa_initialized = false;
bool mouse_aoa_initialized = false;
#endif
#ifdef HAVE_MPRIS
bool mpris_initialized = false;
#endif
bool controller_initialized = false;
bool controller_started = false;
@ -780,6 +789,13 @@ scrcpy(struct scrcpy_options *options) {
}
#endif
#ifdef HAVE_MPRIS
if (!sc_mpris_start(&s->mpris)) {
goto end;
}
mpris_initialized = true;
#endif
// Now that the header values have been consumed, the socket(s) will
// receive the stream(s). Start the demuxer(s).
@ -903,6 +919,12 @@ end:
}
#endif
#ifdef HAVE_MPRIS
if(mpris_initialized) {
sc_mpris_stop(&s->mpris);
}
#endif
#ifdef HAVE_USB
if (aoa_hid_initialized) {
sc_aoa_join(&s->aoa);

View file

@ -867,6 +867,10 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
}
return true;
}
case SC_EVENT_RAISE_WINDOW: {
SDL_RaiseWindow(screen->window);
return true;
}
case SDL_WINDOWEVENT:
if (!screen->video
&& event->window.event == SDL_WINDOWEVENT_EXPOSED) {

View file

@ -6,3 +6,4 @@ option('server_debugger', type: 'boolean', value: false, description: 'Run a ser
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')
option('mpris', type: 'boolean', value: true, description: 'Enable DBus MPRIS when supported')