1144 lines
40 KiB
C
1144 lines
40 KiB
C
#include <gio/gio.h>
|
|
#include <glib-unix.h>
|
|
#include <mpv/client.h>
|
|
#include <libavformat/avformat.h>
|
|
#include <inttypes.h>
|
|
#include <string.h>
|
|
|
|
|
|
|
|
static const char *introspection_xml =
|
|
"<node>\n"
|
|
" <interface name=\"org.mpris.MediaPlayer2\">\n"
|
|
" <method name=\"Raise\">\n"
|
|
" </method>\n"
|
|
" <method name=\"Quit\">\n"
|
|
" </method>\n"
|
|
" <property name=\"CanQuit\" type=\"b\" access=\"read\"/>\n"
|
|
" <property name=\"Fullscreen\" type=\"b\" access=\"readwrite\"/>\n"
|
|
" <property name=\"CanSetFullscreen\" type=\"b\" access=\"read\"/>\n"
|
|
" <property name=\"CanRaise\" type=\"b\" access=\"read\"/>\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>\n"
|
|
" <method name=\"Previous\">\n"
|
|
" </method>\n"
|
|
" <method name=\"Pause\">\n"
|
|
" </method>\n"
|
|
" <method name=\"PlayPause\">\n"
|
|
" </method>\n"
|
|
" <method name=\"Stop\">\n"
|
|
" </method>\n"
|
|
" <method name=\"Play\">\n"
|
|
" </method>\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";
|
|
|
|
typedef struct UserData
|
|
{
|
|
mpv_handle *mpv;
|
|
GMainLoop *loop;
|
|
gint bus_id;
|
|
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;
|
|
gboolean seek_expected;
|
|
gboolean idle;
|
|
gboolean paused;
|
|
} UserData;
|
|
|
|
static const char *STATUS_PLAYING = "Playing";
|
|
static const char *STATUS_PAUSED = "Paused";
|
|
static const char *STATUS_STOPPED = "Stopped";
|
|
static const char *LOOP_NONE = "None";
|
|
static const char *LOOP_TRACK = "Track";
|
|
static const char *LOOP_PLAYLIST = "Playlist";
|
|
|
|
static gchar *string_to_utf8(gchar *maybe_utf8)
|
|
{
|
|
gchar *attempted_validation;
|
|
attempted_validation = g_utf8_make_valid(maybe_utf8, -1);
|
|
|
|
if (g_utf8_validate(attempted_validation, -1, NULL)) {
|
|
return attempted_validation;
|
|
} else {
|
|
g_free(attempted_validation);
|
|
return g_strdup("<invalid utf8>");
|
|
}
|
|
}
|
|
|
|
static void add_metadata_item_string(mpv_handle *mpv, GVariantDict *dict,
|
|
const char *property, const char *tag)
|
|
{
|
|
char *temp = mpv_get_property_string(mpv, property);
|
|
if (temp) {
|
|
char *utf8 = string_to_utf8(temp);
|
|
g_variant_dict_insert(dict, tag, "s", utf8);
|
|
g_free(utf8);
|
|
mpv_free(temp);
|
|
}
|
|
}
|
|
|
|
static void add_metadata_item_int(mpv_handle *mpv, GVariantDict *dict,
|
|
const char *property, const char *tag)
|
|
{
|
|
int64_t value;
|
|
int res = mpv_get_property(mpv, property, MPV_FORMAT_INT64, &value);
|
|
if (res >= 0) {
|
|
g_variant_dict_insert(dict, tag, "x", value);
|
|
}
|
|
}
|
|
|
|
static void add_metadata_item_string_list(mpv_handle *mpv, GVariantDict *dict,
|
|
const char *property, const char *tag)
|
|
{
|
|
char *temp = mpv_get_property_string(mpv, property);
|
|
|
|
if (temp) {
|
|
GVariantBuilder builder;
|
|
char **list = g_strsplit(temp, ", ", 0);
|
|
char **iter = list;
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
|
|
|
|
for (; *iter; iter++) {
|
|
char *item = *iter;
|
|
char *utf8 = string_to_utf8(item);
|
|
g_variant_builder_add(&builder, "s", utf8);
|
|
g_free(utf8);
|
|
}
|
|
|
|
g_variant_dict_insert(dict, tag, "as", &builder);
|
|
|
|
g_strfreev(list);
|
|
mpv_free(temp);
|
|
}
|
|
}
|
|
|
|
static gchar *path_to_uri(mpv_handle *mpv, char *path)
|
|
{
|
|
#if GLIB_CHECK_VERSION(2, 58, 0)
|
|
// version which uses g_canonicalize_filename which expands .. and .
|
|
// and makes the uris neater
|
|
char* working_dir;
|
|
gchar* canonical;
|
|
gchar *uri;
|
|
|
|
working_dir = mpv_get_property_string(mpv, "working-directory");
|
|
canonical = g_canonicalize_filename(path, working_dir);
|
|
uri = g_filename_to_uri(canonical, NULL, NULL);
|
|
|
|
mpv_free(working_dir);
|
|
g_free(canonical);
|
|
|
|
return uri;
|
|
#else
|
|
// for compatibility with older versions of glib
|
|
gchar *converted;
|
|
if (g_path_is_absolute(path)) {
|
|
converted = g_filename_to_uri(path, NULL, NULL);
|
|
} else {
|
|
char* working_dir;
|
|
gchar* absolute;
|
|
|
|
working_dir = mpv_get_property_string(mpv, "working-directory");
|
|
absolute = g_build_filename(working_dir, path, NULL);
|
|
converted = g_filename_to_uri(absolute, NULL, NULL);
|
|
|
|
mpv_free(working_dir);
|
|
g_free(absolute);
|
|
}
|
|
|
|
return converted;
|
|
#endif
|
|
}
|
|
|
|
static void add_metadata_uri(mpv_handle *mpv, GVariantDict *dict)
|
|
{
|
|
char *path;
|
|
char *uri;
|
|
|
|
path = mpv_get_property_string(mpv, "path");
|
|
if (!path) {
|
|
return;
|
|
}
|
|
|
|
uri = g_uri_parse_scheme(path);
|
|
if (uri) {
|
|
g_variant_dict_insert(dict, "xesam:url", "s", path);
|
|
g_free(uri);
|
|
} else {
|
|
gchar *converted = path_to_uri(mpv, path);
|
|
g_variant_dict_insert(dict, "xesam:url", "s", converted);
|
|
g_free(converted);
|
|
}
|
|
|
|
mpv_free(path);
|
|
}
|
|
|
|
// Copied from https://github.com/videolan/vlc/blob/master/modules/meta_engine/folder.c
|
|
static const char art_files[][20] = {
|
|
"Folder.jpg", /* Windows */
|
|
"Folder.png",
|
|
"AlbumArtSmall.jpg", /* Windows */
|
|
"AlbumArt.jpg", /* Windows */
|
|
"Album.jpg",
|
|
".folder.png", /* KDE? */
|
|
"cover.jpg", /* rockbox */
|
|
"cover.png",
|
|
"cover.gif",
|
|
"front.jpg",
|
|
"front.png",
|
|
"front.gif",
|
|
"front.bmp",
|
|
"thumb.jpg",
|
|
};
|
|
|
|
static const int art_files_count = sizeof(art_files) / sizeof(art_files[0]);
|
|
|
|
static gchar* try_get_local_art(mpv_handle *mpv, char *path)
|
|
{
|
|
gchar *dirname = g_path_get_dirname(path), *out = NULL;
|
|
gboolean found = FALSE;
|
|
|
|
for (int i = 0; i < art_files_count; i++) {
|
|
gchar *filename = g_build_filename(dirname, art_files[i], NULL);
|
|
|
|
if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
|
|
out = path_to_uri(mpv, filename);
|
|
found = TRUE;
|
|
}
|
|
|
|
g_free(filename);
|
|
|
|
if (found) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_free(dirname);
|
|
return out;
|
|
}
|
|
|
|
static const char *youtube_url_pattern =
|
|
"^https?:\\/\\/(?:youtu.be\\/|(?:www\\.)?youtube\\.com\\/watch\\?v=)(?<id>[a-zA-Z0-9_-]*)\\??.*";
|
|
|
|
static GRegex *youtube_url_regex;
|
|
|
|
static gchar* try_get_youtube_thumbnail(char *path)
|
|
{
|
|
gchar *out = NULL;
|
|
if (!youtube_url_regex) {
|
|
youtube_url_regex = g_regex_new(youtube_url_pattern, 0, 0, NULL);
|
|
}
|
|
|
|
GMatchInfo *match_info;
|
|
gboolean matched = g_regex_match(youtube_url_regex, path, 0, &match_info);
|
|
|
|
if (matched) {
|
|
gchar *video_id = g_match_info_fetch_named(match_info, "id");
|
|
out = g_strconcat("https://i1.ytimg.com/vi/",
|
|
video_id, "/hqdefault.jpg", NULL);
|
|
g_free(video_id);
|
|
}
|
|
|
|
g_match_info_free(match_info);
|
|
return out;
|
|
}
|
|
|
|
static gchar* extract_embedded_art(AVFormatContext *context) {
|
|
AVPacket *packet = NULL;
|
|
for (unsigned int i = 0; i < context->nb_streams; i++) {
|
|
if (context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) {
|
|
packet = &context->streams[i]->attached_pic;
|
|
}
|
|
}
|
|
if (!packet) {
|
|
return NULL;
|
|
}
|
|
|
|
gchar *data = g_base64_encode(packet->data, packet->size);
|
|
gchar *img = g_strconcat("data:image/jpeg;base64,", data, NULL);
|
|
|
|
g_free(data);
|
|
return img;
|
|
}
|
|
|
|
static gchar* try_get_embedded_art(char *path)
|
|
{
|
|
gchar *out = NULL;
|
|
AVFormatContext *context = NULL;
|
|
if (!avformat_open_input(&context, path, NULL, NULL)) {
|
|
out = extract_embedded_art(context);
|
|
avformat_close_input(&context);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
// cached last file path, owned by mpv
|
|
static char *cached_path = NULL;
|
|
|
|
// cached last artwork url, owned by glib
|
|
static gchar *cached_art_url = NULL;
|
|
|
|
static void add_metadata_art(mpv_handle *mpv, GVariantDict *dict)
|
|
{
|
|
char *path = mpv_get_property_string(mpv, "path");
|
|
|
|
if (!path) {
|
|
return;
|
|
}
|
|
|
|
// mpv may call create_metadata multiple times, so cache to save CPU
|
|
if (!cached_path || strcmp(path, cached_path)) {
|
|
mpv_free(cached_path);
|
|
g_free(cached_art_url);
|
|
cached_path = path;
|
|
|
|
if (g_str_has_prefix(path, "http")) {
|
|
cached_art_url = try_get_youtube_thumbnail(path);
|
|
} else {
|
|
cached_art_url = try_get_embedded_art(path);
|
|
if (!cached_art_url) {
|
|
cached_art_url = try_get_local_art(mpv, path);
|
|
}
|
|
}
|
|
} else {
|
|
mpv_free(path);
|
|
}
|
|
|
|
if (cached_art_url) {
|
|
g_variant_dict_insert(dict, "mpris:artUrl", "s", cached_art_url);
|
|
}
|
|
}
|
|
|
|
static void add_metadata_content_created(mpv_handle *mpv, GVariantDict *dict)
|
|
{
|
|
char *date_str = mpv_get_property_string(mpv, "metadata/by-key/Date");
|
|
|
|
if (!date_str) {
|
|
return;
|
|
}
|
|
|
|
GDate* date = g_date_new();
|
|
if (strlen(date_str) == 4) {
|
|
gint64 year = g_ascii_strtoll(date_str, NULL, 10);
|
|
if (year != 0) {
|
|
g_date_set_dmy(date, 1, 1, year);
|
|
}
|
|
} else {
|
|
g_date_set_parse(date, date_str);
|
|
}
|
|
|
|
if (g_date_valid(date)) {
|
|
gchar iso8601[20];
|
|
g_date_strftime(iso8601, 20, "%Y-%m-%dT00:00:00Z", date);
|
|
g_variant_dict_insert(dict, "xesam:contentCreated", "s", iso8601);
|
|
}
|
|
|
|
g_date_free(date);
|
|
mpv_free(date_str);
|
|
}
|
|
|
|
static GVariant *create_metadata(UserData *ud)
|
|
{
|
|
GVariantDict dict;
|
|
int64_t track;
|
|
double duration;
|
|
char *temp_str;
|
|
int res;
|
|
|
|
g_variant_dict_init(&dict, NULL);
|
|
|
|
// mpris:trackid
|
|
mpv_get_property(ud->mpv, "playlist-pos", MPV_FORMAT_INT64, &track);
|
|
// playlist-pos < 0 if there is no playlist or current track
|
|
if (track < 0) {
|
|
temp_str = g_strdup("/noplaylist");
|
|
} else {
|
|
temp_str = g_strdup_printf("/%" PRId64, track);
|
|
}
|
|
g_variant_dict_insert(&dict, "mpris:trackid", "o", temp_str);
|
|
g_free(temp_str);
|
|
|
|
// mpris:length
|
|
res = mpv_get_property(ud->mpv, "duration", MPV_FORMAT_DOUBLE, &duration);
|
|
if (res == MPV_ERROR_SUCCESS) {
|
|
g_variant_dict_insert(&dict, "mpris:length", "x", (int64_t)(duration * 1000000.0));
|
|
}
|
|
|
|
// initial value. Replaced with metadata value if available
|
|
add_metadata_item_string(ud->mpv, &dict, "media-title", "xesam:title");
|
|
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Title", "xesam:title");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Album", "xesam:album");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Genre", "xesam:genre");
|
|
|
|
/* Musicbrainz metadata mappings
|
|
(https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html) */
|
|
|
|
// IDv3 metadata format
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Artist Id", "mb:artistId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Track Id", "mb:trackId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Artist Id", "mb:albumArtistId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Id", "mb:albumId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Release Track Id", "mb:releaseTrackId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Work Id", "mb:workId");
|
|
|
|
// Vorbis & APEv2 metadata format
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ARTISTID", "mb:artistId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_TRACKID", "mb:trackId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMARTISTID", "mb:albumArtistId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMID", "mb:albumId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_RELEASETRACKID", "mb:releaseTrackId");
|
|
add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_WORKID", "mb:workId");
|
|
|
|
add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/uploader", "xesam:artist");
|
|
add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Artist", "xesam:artist");
|
|
add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Album_Artist", "xesam:albumArtist");
|
|
add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Composer", "xesam:composer");
|
|
|
|
add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Track", "xesam:trackNumber");
|
|
add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Disc", "xesam:discNumber");
|
|
|
|
add_metadata_uri(ud->mpv, &dict);
|
|
add_metadata_art(ud->mpv, &dict);
|
|
add_metadata_content_created(ud->mpv, &dict);
|
|
|
|
return g_variant_dict_end(&dict);
|
|
}
|
|
|
|
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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
if (g_strcmp0(method_name, "Quit") == 0) {
|
|
const char *cmd[] = {"quit", NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Raise") == 0) {
|
|
// Can't raise
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else {
|
|
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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
GVariant *ret;
|
|
|
|
if (g_strcmp0(property_name, "CanQuit") == 0) {
|
|
ret = g_variant_new_boolean(TRUE);
|
|
|
|
} else if (g_strcmp0(property_name, "Fullscreen") == 0) {
|
|
int fullscreen;
|
|
mpv_get_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &fullscreen);
|
|
ret = g_variant_new_boolean(fullscreen);
|
|
|
|
} else if (g_strcmp0(property_name, "CanSetFullscreen") == 0) {
|
|
int can_fullscreen;
|
|
mpv_get_property(ud->mpv, "vo-configured", MPV_FORMAT_FLAG, &can_fullscreen);
|
|
ret = g_variant_new_boolean(can_fullscreen);
|
|
|
|
} else if (g_strcmp0(property_name, "CanRaise") == 0) {
|
|
ret = g_variant_new_boolean(FALSE);
|
|
|
|
} 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("mpv");
|
|
|
|
} else if (g_strcmp0(property_name, "DesktopEntry") == 0) {
|
|
ret = g_variant_new_string("mpv");
|
|
|
|
} else if (g_strcmp0(property_name, "SupportedUriSchemes") == 0) {
|
|
GVariantBuilder builder;
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
|
|
g_variant_builder_add(&builder, "s", "ftp");
|
|
g_variant_builder_add(&builder, "s", "http");
|
|
g_variant_builder_add(&builder, "s", "https");
|
|
g_variant_builder_add(&builder, "s", "mms");
|
|
g_variant_builder_add(&builder, "s", "rtmp");
|
|
g_variant_builder_add(&builder, "s", "rtsp");
|
|
g_variant_builder_add(&builder, "s", "sftp");
|
|
g_variant_builder_add(&builder, "s", "smb");
|
|
ret = g_variant_builder_end(&builder);
|
|
|
|
} else if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) {
|
|
GVariantBuilder builder;
|
|
g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
|
|
g_variant_builder_add(&builder, "s", "application/ogg");
|
|
g_variant_builder_add(&builder, "s", "audio/mpeg");
|
|
// TODO add the rest
|
|
ret = g_variant_builder_end(&builder);
|
|
|
|
} 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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
if (g_strcmp0(property_name, "Fullscreen") == 0) {
|
|
int fullscreen;
|
|
g_variant_get(value, "b", &fullscreen);
|
|
mpv_set_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
if (g_strcmp0(method_name, "Pause") == 0) {
|
|
int paused = TRUE;
|
|
mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "PlayPause") == 0) {
|
|
int paused;
|
|
if (ud->status == STATUS_PAUSED) {
|
|
paused = FALSE;
|
|
} else {
|
|
paused = TRUE;
|
|
}
|
|
mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Play") == 0) {
|
|
int paused = FALSE;
|
|
mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Stop") == 0) {
|
|
const char *cmd[] = {"stop", NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Next") == 0) {
|
|
const char *cmd[] = {"playlist_next", NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Previous") == 0) {
|
|
const char *cmd[] = {"playlist_prev", NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "Seek") == 0) {
|
|
int64_t offset_us; // in microseconds
|
|
char *offset_str;
|
|
g_variant_get(parameters, "(x)", &offset_us);
|
|
double offset_s = offset_us / 1000000.0;
|
|
offset_str = g_strdup_printf("%f", offset_s);
|
|
|
|
const char *cmd[] = {"seek", offset_str, NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
g_free(offset_str);
|
|
|
|
} else if (g_strcmp0(method_name, "SetPosition") == 0) {
|
|
int64_t current_id;
|
|
char *object_path;
|
|
double new_position_s;
|
|
int64_t new_position_us;
|
|
|
|
mpv_get_property(ud->mpv, "playlist-pos", MPV_FORMAT_INT64, ¤t_id);
|
|
g_variant_get(parameters, "(&ox)", &object_path, &new_position_us);
|
|
new_position_s = ((float)new_position_us) / 1000000.0; // us -> s
|
|
|
|
if (current_id == g_ascii_strtoll(object_path + 1, NULL, 10)) {
|
|
mpv_set_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &new_position_s);
|
|
}
|
|
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else if (g_strcmp0(method_name, "OpenUri") == 0) {
|
|
char *uri;
|
|
g_variant_get(parameters, "(&s)", &uri);
|
|
const char *cmd[] = {"loadfile", uri, NULL};
|
|
mpv_command_async(ud->mpv, 0, cmd);
|
|
g_dbus_method_invocation_return_value(invocation, NULL);
|
|
|
|
} else {
|
|
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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
GVariant *ret;
|
|
if (g_strcmp0(property_name, "PlaybackStatus") == 0) {
|
|
ret = g_variant_new_string(ud->status);
|
|
|
|
} else if (g_strcmp0(property_name, "LoopStatus") == 0) {
|
|
ret = g_variant_new_string(ud->loop_status);
|
|
|
|
} else if (g_strcmp0(property_name, "Rate") == 0) {
|
|
double rate;
|
|
mpv_get_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate);
|
|
ret = g_variant_new_double(rate);
|
|
|
|
} else if (g_strcmp0(property_name, "Shuffle") == 0) {
|
|
int shuffle;
|
|
mpv_get_property(ud->mpv, "playlist-shuffle", MPV_FORMAT_FLAG, &shuffle);
|
|
ret = g_variant_new_boolean(shuffle);
|
|
|
|
} else if (g_strcmp0(property_name, "Metadata") == 0) {
|
|
if (!ud->metadata) {
|
|
ud->metadata = create_metadata(ud);
|
|
}
|
|
// Increase reference count to prevent it from being freed after returning
|
|
g_variant_ref(ud->metadata);
|
|
ret = ud->metadata;
|
|
|
|
} else if (g_strcmp0(property_name, "Volume") == 0) {
|
|
double volume;
|
|
mpv_get_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &volume);
|
|
volume /= 100;
|
|
ret = g_variant_new_double(volume);
|
|
|
|
} else if (g_strcmp0(property_name, "Position") == 0) {
|
|
double position_s;
|
|
int64_t position_us;
|
|
mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s);
|
|
position_us = position_s * 1000000.0; // s -> us
|
|
ret = g_variant_new_int64(position_us);
|
|
|
|
} else if (g_strcmp0(property_name, "MinimumRate") == 0) {
|
|
ret = g_variant_new_double(0.01);
|
|
|
|
} 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(TRUE);
|
|
|
|
} 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,
|
|
gpointer user_data)
|
|
{
|
|
UserData *ud = (UserData*)user_data;
|
|
if (g_strcmp0(property_name, "LoopStatus") == 0) {
|
|
const char *status;
|
|
int t = TRUE;
|
|
int f = FALSE;
|
|
status = g_variant_get_string(value, NULL);
|
|
if (g_strcmp0(status, "Track") == 0) {
|
|
mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &t);
|
|
mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f);
|
|
} else if (g_strcmp0(status, "Playlist") == 0) {
|
|
mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f);
|
|
mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &t);
|
|
} else {
|
|
mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f);
|
|
mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f);
|
|
}
|
|
|
|
} else if (g_strcmp0(property_name, "Rate") == 0) {
|
|
double rate = g_variant_get_double(value);
|
|
mpv_set_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate);
|
|
|
|
} else if (g_strcmp0(property_name, "Shuffle") == 0) {
|
|
int shuffle = g_variant_get_boolean(value);
|
|
mpv_set_property(ud->mpv, "playlist-shuffle", MPV_FORMAT_FLAG, &shuffle);
|
|
|
|
} else if (g_strcmp0(property_name, "Volume") == 0) {
|
|
double volume = g_variant_get_double(value);
|
|
volume *= 100;
|
|
mpv_set_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &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 gboolean emit_property_changes(gpointer data)
|
|
{
|
|
UserData *ud = (UserData*)data;
|
|
GError *error = NULL;
|
|
gpointer prop_name, prop_value;
|
|
GHashTableIter iter;
|
|
|
|
if (g_hash_table_size(ud->changed_properties) > 0) {
|
|
GVariant *params;
|
|
GVariantBuilder *properties = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
|
|
GVariantBuilder *invalidated = g_variant_builder_new(G_VARIANT_TYPE("as"));
|
|
g_hash_table_iter_init(&iter, ud->changed_properties);
|
|
while (g_hash_table_iter_next(&iter, &prop_name, &prop_value)) {
|
|
if (prop_value) {
|
|
g_variant_builder_add(properties, "{sv}", prop_name, prop_value);
|
|
} else {
|
|
g_variant_builder_add(invalidated, "s", prop_name);
|
|
}
|
|
}
|
|
params = g_variant_new("(sa{sv}as)",
|
|
"org.mpris.MediaPlayer2.Player", properties, invalidated);
|
|
g_variant_builder_unref(properties);
|
|
g_variant_builder_unref(invalidated);
|
|
|
|
g_dbus_connection_emit_signal(ud->connection, NULL,
|
|
"/org/mpris/MediaPlayer2",
|
|
"org.freedesktop.DBus.Properties",
|
|
"PropertiesChanged",
|
|
params, &error);
|
|
if (error != NULL) {
|
|
g_printerr("%s", error->message);
|
|
}
|
|
|
|
g_hash_table_remove_all(ud->changed_properties);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static void emit_seeked_signal(UserData *ud)
|
|
{
|
|
GVariant *params;
|
|
double position_s;
|
|
int64_t position_us;
|
|
GError *error = NULL;
|
|
mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s);
|
|
position_us = position_s * 1000000.0; // s -> us
|
|
params = g_variant_new("(x)", position_us);
|
|
|
|
g_dbus_connection_emit_signal(ud->connection, NULL,
|
|
"/org/mpris/MediaPlayer2",
|
|
"org.mpris.MediaPlayer2.Player",
|
|
"Seeked",
|
|
params, &error);
|
|
|
|
if (error != NULL) {
|
|
g_printerr("%s", error->message);
|
|
}
|
|
}
|
|
|
|
static GVariant * set_playback_status(UserData *ud)
|
|
{
|
|
if (ud->idle) {
|
|
ud->status = STATUS_STOPPED;
|
|
} else if (ud->paused) {
|
|
ud->status = STATUS_PAUSED;
|
|
} else {
|
|
ud->status = STATUS_PLAYING;
|
|
}
|
|
return g_variant_new_string(ud->status);
|
|
}
|
|
|
|
static void set_stopped_status(UserData *ud)
|
|
{
|
|
const char *prop_name = "PlaybackStatus";
|
|
GVariant *prop_value = g_variant_new_string(STATUS_STOPPED);
|
|
|
|
ud->status = STATUS_STOPPED;
|
|
|
|
g_hash_table_insert(ud->changed_properties,
|
|
(gpointer)prop_name, prop_value);
|
|
|
|
emit_property_changes(ud);
|
|
}
|
|
|
|
// Register D-Bus object and interfaces
|
|
static void on_bus_acquired(GDBusConnection *connection,
|
|
G_GNUC_UNUSED const char *name,
|
|
gpointer user_data)
|
|
{
|
|
GError *error = NULL;
|
|
UserData *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) {
|
|
UserData *ud = user_data;
|
|
pid_t pid = getpid();
|
|
char *name = g_strdup_printf("org.mpris.MediaPlayer2.mpv.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 void handle_property_change(const char *name, void *data, UserData *ud)
|
|
{
|
|
const char *prop_name = NULL;
|
|
GVariant *prop_value = NULL;
|
|
if (g_strcmp0(name, "pause") == 0) {
|
|
ud->paused = *(int*)data;
|
|
prop_name = "PlaybackStatus";
|
|
prop_value = set_playback_status(ud);
|
|
|
|
} else if (g_strcmp0(name, "idle-active") == 0) {
|
|
ud->idle = *(int*)data;
|
|
prop_name = "PlaybackStatus";
|
|
prop_value = set_playback_status(ud);
|
|
|
|
} else if (g_strcmp0(name, "media-title") == 0 ||
|
|
g_strcmp0(name, "duration") == 0) {
|
|
// Free existing metadata object
|
|
if (ud->metadata) {
|
|
g_variant_unref(ud->metadata);
|
|
}
|
|
ud->metadata = create_metadata(ud);
|
|
prop_name = "Metadata";
|
|
prop_value = ud->metadata;
|
|
|
|
} else if (g_strcmp0(name, "speed") == 0) {
|
|
double *rate = data;
|
|
prop_name = "Rate";
|
|
prop_value = g_variant_new_double(*rate);
|
|
|
|
} else if (g_strcmp0(name, "volume") == 0) {
|
|
double *volume = data;
|
|
*volume /= 100;
|
|
prop_name = "Volume";
|
|
prop_value = g_variant_new_double(*volume);
|
|
|
|
} else if (g_strcmp0(name, "loop-file") == 0) {
|
|
char *status = *(char **)data;
|
|
if (g_strcmp0(status, "no") != 0) {
|
|
ud->loop_status = LOOP_TRACK;
|
|
} else {
|
|
char *playlist_status;
|
|
mpv_get_property(ud->mpv, "loop-playlist", MPV_FORMAT_STRING, &playlist_status);
|
|
if (g_strcmp0(playlist_status, "no") != 0) {
|
|
ud->loop_status = LOOP_PLAYLIST;
|
|
} else {
|
|
ud->loop_status = LOOP_NONE;
|
|
}
|
|
mpv_free(playlist_status);
|
|
}
|
|
prop_name = "LoopStatus";
|
|
prop_value = g_variant_new_string(ud->loop_status);
|
|
|
|
} else if (g_strcmp0(name, "loop-playlist") == 0) {
|
|
char *status = *(char **)data;
|
|
if (g_strcmp0(status, "no") != 0) {
|
|
ud->loop_status = LOOP_PLAYLIST;
|
|
} else {
|
|
char *file_status;
|
|
mpv_get_property(ud->mpv, "loop-file", MPV_FORMAT_STRING, &file_status);
|
|
if (g_strcmp0(file_status, "no") != 0) {
|
|
ud->loop_status = LOOP_TRACK;
|
|
} else {
|
|
ud->loop_status = LOOP_NONE;
|
|
}
|
|
mpv_free(file_status);
|
|
}
|
|
prop_name = "LoopStatus";
|
|
prop_value = g_variant_new_string(ud->loop_status);
|
|
|
|
} else if (g_strcmp0(name, "fullscreen") == 0) {
|
|
gboolean *status = data;
|
|
prop_name = "Fullscreen";
|
|
prop_value = g_variant_new_boolean(*status);
|
|
|
|
}
|
|
|
|
if (prop_name) {
|
|
if (prop_value) {
|
|
g_variant_ref(prop_value);
|
|
}
|
|
g_hash_table_insert(ud->changed_properties,
|
|
(gpointer)prop_name, prop_value);
|
|
}
|
|
}
|
|
|
|
static gboolean event_handler(int fd, G_GNUC_UNUSED GIOCondition condition, gpointer data)
|
|
{
|
|
UserData *ud = data;
|
|
gboolean has_event = TRUE;
|
|
|
|
// Discard data in pipe
|
|
char unused[16];
|
|
while (read(fd, unused, sizeof(unused)) > 0);
|
|
|
|
while (has_event) {
|
|
mpv_event *event = mpv_wait_event(ud->mpv, 0);
|
|
switch (event->event_id) {
|
|
case MPV_EVENT_NONE:
|
|
has_event = FALSE;
|
|
break;
|
|
case MPV_EVENT_SHUTDOWN:
|
|
set_stopped_status(ud);
|
|
g_main_loop_quit(ud->loop);
|
|
break;
|
|
case MPV_EVENT_PROPERTY_CHANGE: {
|
|
mpv_event_property *prop_event = (mpv_event_property*)event->data;
|
|
handle_property_change(prop_event->name, prop_event->data, ud);
|
|
} break;
|
|
case MPV_EVENT_SEEK:
|
|
ud->seek_expected = TRUE;
|
|
break;
|
|
case MPV_EVENT_PLAYBACK_RESTART: {
|
|
if (ud->seek_expected) {
|
|
emit_seeked_signal(ud);
|
|
ud->seek_expected = FALSE;
|
|
}
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void wakeup_handler(void *fd)
|
|
{
|
|
(void)!write(*((int*)fd), "0", 1);
|
|
}
|
|
|
|
// Plugin entry point
|
|
int mpv_open_cplugin(mpv_handle *mpv)
|
|
{
|
|
GMainContext *ctx;
|
|
GMainLoop *loop;
|
|
UserData ud = {0};
|
|
GError *error = NULL;
|
|
GDBusNodeInfo *introspection_data = NULL;
|
|
int pipe[2];
|
|
GSource *mpv_pipe_source;
|
|
GSource *timeout_source;
|
|
|
|
ctx = g_main_context_new();
|
|
loop = g_main_loop_new(ctx, FALSE);
|
|
|
|
// Load introspection data and split into separate interfaces
|
|
introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error);
|
|
if (error != NULL) {
|
|
g_printerr("%s", error->message);
|
|
}
|
|
ud.root_interface_info =
|
|
g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2");
|
|
ud.player_interface_info =
|
|
g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2.Player");
|
|
|
|
ud.mpv = mpv;
|
|
ud.loop = loop;
|
|
ud.status = STATUS_STOPPED;
|
|
ud.loop_status = LOOP_NONE;
|
|
ud.changed_properties = g_hash_table_new(g_str_hash, g_str_equal);
|
|
ud.seek_expected = FALSE;
|
|
ud.idle = FALSE;
|
|
ud.paused = FALSE;
|
|
|
|
g_main_context_push_thread_default(ctx);
|
|
ud.bus_id = g_bus_own_name(G_BUS_TYPE_SESSION,
|
|
"org.mpris.MediaPlayer2.mpv",
|
|
G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
|
|
on_bus_acquired,
|
|
NULL,
|
|
on_name_lost,
|
|
&ud, NULL);
|
|
g_main_context_pop_thread_default(ctx);
|
|
|
|
// Receive event for property changes
|
|
mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
|
|
mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG);
|
|
mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING);
|
|
mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE);
|
|
mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE);
|
|
mpv_observe_property(mpv, 0, "loop-file", MPV_FORMAT_STRING);
|
|
mpv_observe_property(mpv, 0, "loop-playlist", MPV_FORMAT_STRING);
|
|
mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_INT64);
|
|
mpv_observe_property(mpv, 0, "fullscreen", MPV_FORMAT_FLAG);
|
|
|
|
// Run callback whenever there are events
|
|
g_unix_open_pipe(pipe, 0, &error);
|
|
if (error != NULL) {
|
|
g_printerr("%s", error->message);
|
|
}
|
|
fcntl(pipe[0], F_SETFL, O_NONBLOCK);
|
|
mpv_set_wakeup_callback(mpv, wakeup_handler, &pipe[1]);
|
|
mpv_pipe_source = g_unix_fd_source_new(pipe[0], G_IO_IN);
|
|
g_source_set_callback(mpv_pipe_source,
|
|
G_SOURCE_FUNC(event_handler),
|
|
&ud,
|
|
NULL);
|
|
g_source_attach(mpv_pipe_source, ctx);
|
|
|
|
// Emit any new property changes every 100ms
|
|
timeout_source = g_timeout_source_new(100);
|
|
g_source_set_callback(timeout_source,
|
|
G_SOURCE_FUNC(emit_property_changes),
|
|
&ud,
|
|
NULL);
|
|
g_source_attach(timeout_source, ctx);
|
|
|
|
g_main_loop_run(loop);
|
|
|
|
g_source_unref(mpv_pipe_source);
|
|
g_source_unref(timeout_source);
|
|
|
|
g_dbus_connection_unregister_object(ud.connection, ud.root_interface_id);
|
|
g_dbus_connection_unregister_object(ud.connection, ud.player_interface_id);
|
|
|
|
g_bus_unown_name(ud.bus_id);
|
|
g_main_loop_unref(loop);
|
|
g_main_context_unref(ctx);
|
|
g_dbus_node_info_unref(introspection_data);
|
|
|
|
return 0;
|
|
}
|