前言
KDE桌面环境下有一个KDE Connect用来和Android设备交互,包括文件传输等,其中有一个媒体控制,这个场景是在电脑上网页播放视频时,Android可以进行暂停、播放、上/下一首,觉得这个很有意思,就研究了一下。
这个问题主要是:外部程序是怎么控制以及知道网页上播放的视频信息的
这其中涉及到的技术有两个。
-
D-Bus
D-Bus 是一个消息总线系统,用于在 Linux 系统中的不同进程之间进行通信。它提供了一个机制,通过消息的方式让不同的应用程序或系统组件能够进行跨进程通信。
-
MPRIS
MPRIS 是一个在 D-Bus 上构建的规范,它定义了一个标准接口,使媒体播放器能够与其他应用程序或桌面环境交互
其中MPRIS是个规范,那就要有实现,而几个有名的客户端就是Chrome、Firefox、VLC,他们实现了MPRIS规范,在他们播放视频时候,会通过D-Bus在会话总线类别中广播一条信息,称为信号,这里的会话总线是D-Bus中的概念,D-Bus提供了两种类型的通信总线:系统总线(System Bus) 和 会话总线(Session Bus) 。这两种总线具有不同的作用域和使用场景,它们在 D-Bus 中扮演着不同的角色,这里广播只在会话总线中广播,而下面说到的蓝牙设备,是在系统总线中广播。
那么其他应用程序只要监听D-Bus会话总线中这个事件就行,而KDE Connect的系统守护程序,就是监听这个事件后,通过TCP/UDP发送到Android设备,而Android在13后,也增加了媒体统一控制,当系统在播放音乐时,一方面是推送一个他自身的控制界面,另一方面也会推送给Android系统,像我使用的小米,下拉控制栏左面是音乐播放器自身的,右面是系统自身的,都可以达到控制音乐暂停、播放。
那么手机就是收到暂停指令后,在通过TCP/UDP发送到KDE Connect服务器,服务器在通过D-Bus进行方法调用。
这里又提到方法调用,这里类似gRPC,先是服务端定义方法,比如Stop方法、Play方法,然后客户端调用,而D-Bus就是这样做的,他是进程间通信的核心机制,通过 D-Bus,应用程序可以调用其他应用程序暴露的方法,并传递数据。
那么就伴随着方法的ID,比如在Java中,类名+方法名才能定位到具体的方法。
再说一下D-Bus中的几个概念。
-
服务名(Service Name)
服务名是 D-Bus 上唯一标识一个服务的名称。服务名通常采用类似 DNS 域名的格式,例如
org.freedesktop.DBus或org.gnome.SettingsDaemon。 -
对象路径(Object Path)
对象路径指向 D-Bus 服务中提供的某个具体对象
-
接口(Interface)
接口定义了一个对象支持的方法。它类似于编程语言中的接口(Interface),描述了对象可以提供哪些操作。
-
方法(Method)
方法是接口中定义的可调用函数。
而D-Bus是把这些信息注册到系统里面的, 任何程序都可以查看。
播放/暂停
比如下面命令查看所有已经注册的服务。
dbus-send --session --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames
在chrome上播放视频时候,会有这两条记录
string "org.mpris.MediaPlayer2.chromium.instance1354"
string "org.mpris.MediaPlayer2.plasma-browser-integration"
然后可以通过下面命令查看他们定义的方法。
gdbus introspect --session --dest org.mpris.MediaPlayer2.plasma-browser-integration --object-path /org/mpris/MediaPlayer2
output
....
interface org.mpris.MediaPlayer2.Player {
methods:
Next();
Previous();
Pause();
PlayPause();
Stop();
Play();
Seek(in x Offset);
SetPosition(in o TrackId,
in x Position);
OpenUri(in s Uri);
signals:
Seeked(x Position)
这里object-path如果不知道的话,可以传入/,一层层的看。从interface里面可以看到有定义Next、Previous、Pause、Play等控制播放的方法。
知道服务名、对象路径、接口后在通过下面命令调用具体方法,gdbus是其中一个,还可以通过dbus-send,下面两条命令就是用来控制chrom播放暂停
gdbus call --session --dest org.mpris.MediaPlayer2.chromium.instance1354 --object-path /org/mpris/MediaPlayer2 --method org.mpris.MediaPlayer2.Player.Play
dbus-send --session --dest=org.mpris.MediaPlayer2.chromium.instance1354 --print-reply /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Pause
事件监听
当chrome播放/暂停等事件发生时,会通过D-Bus广播,我们只要监听就行,下面用c语言写一个例子。
这个例子不光能监听播放暂停、还能监听出视频标题。
#include <dbus/dbus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void parse_dbus_message(DBusMessage *message) {
DBusMessageIter iter, array_iter, dict_iter, variant_iter, entry_iter;
const char *key;
dbus_bool_t can_pause, can_play;
const char *playback_status;
dbus_int64_t position;
dbus_message_iter_init(message, &iter);
const char *str;
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {
dbus_message_iter_get_basic(&iter, &str);
if (strcmp(str, "org.mpris.MediaPlayer2.Player") != 0) {
return;
}
}
if (!dbus_message_iter_next(&iter))return;
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY) {
dbus_message_iter_recurse(&iter, &array_iter);
while (dbus_message_iter_get_arg_type(&array_iter) == DBUS_TYPE_DICT_ENTRY) {
dbus_message_iter_recurse(&array_iter, &dict_iter);
dbus_message_iter_get_basic(&dict_iter, &key);
dbus_message_iter_next(&dict_iter);
dbus_message_iter_recurse(&dict_iter, &variant_iter);
if (strcmp(key, "CanPause") == 0) {
dbus_message_iter_get_basic(&variant_iter, &can_pause);
} else if (strcmp(key, "CanPlay") == 0) {
dbus_message_iter_get_basic(&variant_iter, &can_play);
} else if (strcmp(key, "PlaybackStatus") == 0) {
dbus_message_iter_get_basic(&variant_iter, &playback_status);
} else if (strcmp(key, "Position") == 0) {
dbus_message_iter_get_basic(&variant_iter, &position);
} else if (strcmp(key, "Metadata") == 0) {
dbus_message_iter_recurse(&variant_iter, &array_iter);
while (dbus_message_iter_get_arg_type(&array_iter) == DBUS_TYPE_DICT_ENTRY) {
dbus_message_iter_recurse(&array_iter, &entry_iter);
dbus_message_iter_get_basic(&entry_iter, &key);
dbus_message_iter_next(&entry_iter);
dbus_message_iter_recurse(&entry_iter, &variant_iter);
if (strcmp(key, "kde:pid") == 0) {
dbus_int32_t pid;
dbus_message_iter_get_basic(&variant_iter, &pid);
} else if (strcmp(key, "mpris:length") == 0) {
dbus_int64_t length;
dbus_message_iter_get_basic(&variant_iter, &length);
} else if (strcmp(key, "xesam:title") == 0) {
const char *title;
dbus_message_iter_get_basic(&variant_iter, &title);
printf("%s\n", title);
}
dbus_message_iter_next(&array_iter);
}
}
dbus_message_iter_next(&array_iter);
}
}
if (playback_status == NULL )return;
printf("state:%s,\n", playback_status);
}
int main() {
DBusError error;
DBusConnection *connection;
connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
dbus_bus_add_match(
connection, "path='/org/mpris/MediaPlayer2',type='signal',interface='org.freedesktop.DBus.Properties'", &error);
if (dbus_error_is_set(&error)) {
fprintf(stderr, "Match Error (%s)\n", error.message);
exit(1);
}
for (;;) {
dbus_connection_read_write(connection, 0);
DBusMessage *msg = dbus_connection_pop_message(connection);
if (msg == NULL) {
continue;
}
if (dbus_message_is_signal(msg, "org.freedesktop.DBus.Properties", "PropertiesChanged")) {
DBusError err;
dbus_error_init(&err);
parse_dbus_message(msg);
}
dbus_message_unref(msg);
}
}
有一个命令行工具,可以监听出系统所有总线消息,可以根据这个输出来编写我们的代码。
其中--session是监听会话总线的消息,还有--system监听系统总线。
dbus-monitor --session
蓝牙设备
Linux下的蓝牙服务也提供了D-Bus接口,可以通过其控制蓝牙,发送AVRCP命令使其播放/暂停。
首先第一步就是查找已经连接的蓝牙信息,通过下面命令。
dbus-send --system --dest=org.bluez --print-reply / org.freedesktop.DBus.ObjectManager.GetManagedObjects
找到其中Connected属性为true的,则表示是连接的蓝牙,他上面会有一个object-path,根据这个才能操作指定的蓝牙。
dict entry(
string "org.bluez.MediaControl1"
array [
dict entry(
string "Connected"
variant boolean true
)
dict entry(
string "Player"
variant object path "/org/bluez/hci0/dev_A4_55_90_AD_3C_CE/player0"
)
]
)
比如下面两条命令就是控制暂停/播放,其中/org/bluez/hci0/dev_A4_55_90_AD_3C_CE就是从上面输出的object-path的来。
dbus-send --system --type=method_call --dest=org.bluez /org/bluez/hci0/dev_A4_55_90_AD_3C_CE org.bluez.MediaControl1.Play
dbus-send --system --type=method_call --dest=org.bluez /org/bluez/hci0/dev_A4_55_90_AD_3C_CE org.bluez.MediaControl1.Pause