[RFC] prevent-dpms-off: Add module to signal DPMS should not happen

Submitted by Benjamin Berg on Feb. 19, 2018, 10:13 a.m.

Details

Message ID 20180219101354.25205-1-benjamin@sipsolutions.net
State New
Headers show
Series "prevent-dpms-off: Add module to signal DPMS should not happen" ( rev: 1 ) in PulseAudio

Not browsing as part of any series.

Commit Message

Benjamin Berg Feb. 19, 2018, 10:13 a.m.
From: Benjamin Berg <bberg@redhat.com>

Whenever an HDMI/DP sink is in use DPMS should not be done by desktop
environments. Add a property to the appropriate ports and inform the DE
that DPMS off should be inhibited whenever a port or sink with the
device.requires.no_dpms_off property is set to boolean true.

TODO: Implement protocol to the DE to signal the situation
---

This is about bug
  https://bugs.freedesktop.org/show_bug.cgi?id=97866

As the commit message says, the patch is incomplete and we would also
need a way for desktop environments to consume the information. I could
both see pushing this information by grabbing an Inhibitor at a known DBus
path or making it possibly to query the information from pulseaudio.

Does anyone have an opinion as to what the protocol should look like?

On the GNOME side, this information would be either consumed by Mutter
directly or by gnome-settings-daemon as that is initiating screen
blanking. I don't know about other DEs.

 src/Makefile.am                                 |   7 +
 src/modules/alsa/mixer/paths/hdmi-output-0.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-1.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-2.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-3.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-4.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-5.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-6.conf |   1 +
 src/modules/alsa/mixer/paths/hdmi-output-7.conf |   1 +
 src/modules/module-prevent-dpms-off.c           | 273 ++++++++++++++++++++++++
 src/pulse/proplist.h                            |   3 +
 11 files changed, 291 insertions(+)
 create mode 100644 src/modules/module-prevent-dpms-off.c

Patch hide | download patch | download mbox

diff --git a/src/Makefile.am b/src/Makefile.am
index 45616c01..dab087bd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1201,6 +1201,7 @@  modlibexec_LTLIBRARIES += \
 		module-rescue-streams.la \
 		module-intended-roles.la \
 		module-suspend-on-idle.la \
+		module-prevent-dpms-off.la \
 		module-echo-cancel.la \
 		module-http-protocol-tcp.la \
 		module-sine.la \
@@ -1967,6 +1968,12 @@  module_suspend_on_idle_la_LDFLAGS = $(MODULE_LDFLAGS)
 module_suspend_on_idle_la_LIBADD = $(MODULE_LIBADD)
 module_suspend_on_idle_la_CFLAGS = $(AM_CFLAGS) -DPA_MODULE_NAME=module_suspend_on_idle
 
+# Prevent-DPMS-off module
+module_prevent_dpms_off_la_SOURCES = modules/module-prevent-dpms-off.c
+module_prevent_dpms_off_la_LDFLAGS = $(MODULE_LDFLAGS)
+module_prevent_dpms_off_la_LIBADD = $(MODULE_LIBADD)
+module_prevent_dpms_off_la_CFLAGS = $(AM_CFLAGS)
+
 # echo-cancel module
 module_echo_cancel_la_SOURCES = \
 		modules/echo-cancel/module-echo-cancel.c \
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-0.conf b/src/modules/alsa/mixer/paths/hdmi-output-0.conf
index 95b1342e..5548e778 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-0.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-0.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-1.conf b/src/modules/alsa/mixer/paths/hdmi-output-1.conf
index 37b94520..4e76b658 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-1.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-1.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-2.conf b/src/modules/alsa/mixer/paths/hdmi-output-2.conf
index 19c38f2e..49cf9410 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-2.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-2.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-3.conf b/src/modules/alsa/mixer/paths/hdmi-output-3.conf
index 8551570a..7ba581f8 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-3.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-3.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-4.conf b/src/modules/alsa/mixer/paths/hdmi-output-4.conf
index e3612892..1cdf8dfd 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-4.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-4.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-5.conf b/src/modules/alsa/mixer/paths/hdmi-output-5.conf
index 82dc3be7..3ee094af 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-5.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-5.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-6.conf b/src/modules/alsa/mixer/paths/hdmi-output-6.conf
index 92e8fd1e..3d127b7c 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-6.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-6.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/alsa/mixer/paths/hdmi-output-7.conf b/src/modules/alsa/mixer/paths/hdmi-output-7.conf
index abe2b60e..11425b1f 100644
--- a/src/modules/alsa/mixer/paths/hdmi-output-7.conf
+++ b/src/modules/alsa/mixer/paths/hdmi-output-7.conf
@@ -5,6 +5,7 @@  eld-device = auto
 
 [Properties]
 device.icon_name = video-display
+device.requires.no_dpms_off = 1
 
 [Jack HDMI/DP]
 append-pcm-to-name = yes
diff --git a/src/modules/module-prevent-dpms-off.c b/src/modules/module-prevent-dpms-off.c
new file mode 100644
index 00000000..702aa215
--- /dev/null
+++ b/src/modules/module-prevent-dpms-off.c
@@ -0,0 +1,273 @@ 
+/***
+  This file is part of PulseAudio.
+
+  Copyright 2006 Lennart Poettering
+  Copyright 2017 Benjamin Berg
+
+  PulseAudio is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published
+  by the Free Software Foundation; either version 2.1 of the License,
+  or (at your option) any later version.
+
+  PulseAudio is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pulse/xmalloc.h>
+#include <pulse/timeval.h>
+#include <pulse/rtclock.h>
+
+#include <pulsecore/core.h>
+#include <pulsecore/core-util.h>
+#include <pulsecore/sink-input.h>
+#include <pulsecore/source-output.h>
+#include <pulsecore/modargs.h>
+#include <pulsecore/log.h>
+
+#include "module-prevent-dpms-off-symdef.h"
+
+PA_MODULE_AUTHOR("Benjamin Berg");
+PA_MODULE_DESCRIPTION("Signal to graphical session that displays should not be suspended using DPMS");
+PA_MODULE_VERSION(PACKAGE_VERSION);
+PA_MODULE_LOAD_ONCE(true);
+
+/*
+ * This module signals that DPMS should not happen if a sink is not suspended
+ * and device.requires.no_dpms_off is set on the active port (prefered) or on
+ * the sink/source itself.
+ *
+ * Note that the sink might not be suspended for short periods of time even
+ * though no playback is happening. This module does not special case these
+ * corner cases for the sake of simplicity.
+ *
+ * As such the following events are relevant:
+ *  - SINK/SOURCE state change
+ *  - SINK/SOURCE unlinking
+ *  - SINK/SOURCE port changes
+ *  - SINK/SOURCE proplist changes
+ *
+ * The device.requires.no_dpms_off property is only checked when a sink/source
+ * becomes active.
+ */
+
+static const char* const valid_modargs[] = {
+    NULL,
+};
+
+struct userdata {
+    pa_core *core;
+    pa_hashmap *inhibit_infos;
+};
+
+struct dpms_inhibit_info {
+    struct userdata *userdata;
+    pa_sink *sink;
+    pa_source *source;
+
+    /* also store e.g. cookie or similar to uninhibit */
+};
+
+static void inhibit_dpms(struct userdata *u, pa_sink *sink, pa_source *source)
+{
+    struct dpms_inhibit_info *d;
+
+    if (sink && !pa_hashmap_get(u->inhibit_infos, sink)) {
+        d = pa_xnew(struct dpms_inhibit_info, 1);
+
+        d->userdata = u;
+        d->source = NULL;
+        d->sink = pa_sink_ref(sink);
+
+        pa_hashmap_put(u->inhibit_infos, sink, d);
+
+        pa_log_info("Sink %s is now inhibiting DPMS power management of monitors.", d->sink->name);
+    }
+    if (source && (d = pa_hashmap_get(u->inhibit_infos, source))) {
+        d = pa_xnew(struct dpms_inhibit_info, 1);
+
+        d->userdata = u;
+        d->source = pa_source_ref(source);
+        d->sink = NULL;
+
+        pa_hashmap_remove_and_free(u->inhibit_infos, sink);
+
+        pa_log_info("Source %s is now inhibiting DPMS power management of monitors.", d->source->name);
+    }
+}
+
+static void uninhibit_dpms(struct userdata *u, pa_sink *sink, pa_source *source)
+{
+    struct dpms_inhibit_info *d;
+
+    if (sink && (d = pa_hashmap_get(u->inhibit_infos, sink))) {
+        pa_log_info("Sink %s is no longer inhibiting DPMS power management of monitors.", d->sink->name);
+
+        pa_hashmap_remove_and_free(u->inhibit_infos, sink);
+    }
+    if (source && (d = pa_hashmap_get(u->inhibit_infos, source))) {
+        pa_log_info("Source %s is no longer inhibiting DPMS power management of monitors.", d->source->name);
+
+        pa_hashmap_remove_and_free(u->inhibit_infos, sink);
+    }
+}
+
+static void sync_dpms_inhibit_state(struct userdata *u, bool active, pa_sink *sink, pa_source *source)
+{
+    bool inhibit;
+
+    pa_assert(source || sink);
+
+    if (active) {
+        const char *prop_str = NULL;
+        pa_device_port *port;
+        inhibit = true;
+
+        port = sink ? sink->active_port : source->active_port;
+        if (port)
+            prop_str = pa_proplist_gets(port->proplist, PA_PROP_DEVICE_REQUIRES_NO_DPMS_OFF);
+        if (!prop_str)
+            prop_str = pa_proplist_gets(sink ? sink->proplist : source->proplist, PA_PROP_DEVICE_REQUIRES_NO_DPMS_OFF);
+
+        /* Not set or set to boolean FALSE (or invalid). */
+        if (!prop_str || pa_parse_boolean(prop_str) <= 0)
+           inhibit = false;
+    } else {
+        inhibit = false;
+    }
+
+    if (inhibit)
+        inhibit_dpms(u, sink, source);
+    else
+        uninhibit_dpms(u, sink, source);
+}
+
+static pa_hook_result_t device_unlink_hook_cb(pa_core *c, pa_object *o, struct userdata *u) {
+    pa_assert(c);
+    pa_object_assert_ref(o);
+    pa_assert(u);
+
+    if (pa_sink_isinstance(o)) {
+        pa_sink *s = PA_SINK(o);
+
+        uninhibit_dpms(u, s, NULL);
+    } else if (pa_source_isinstance(o)) {
+        pa_source *s = PA_SOURCE(o);
+
+        uninhibit_dpms(u, NULL, s);
+    }
+
+    return PA_HOOK_OK;
+}
+
+static pa_hook_result_t device_recheck_dpms_inhibit_state(pa_core *c, pa_object *o, struct userdata *u) {
+    pa_assert(c);
+    pa_object_assert_ref(o);
+    pa_assert(u);
+
+    if (pa_sink_isinstance(o)) {
+        pa_sink *s = PA_SINK(o);
+        pa_sink_state_t state = pa_sink_get_state(s);
+
+        sync_dpms_inhibit_state(u, PA_SINK_IS_OPENED(state), s, NULL);
+
+    } else if (pa_source_isinstance(o)) {
+        pa_source *s = PA_SOURCE(o);
+        pa_source_state_t state = pa_source_get_state(s);
+
+        sync_dpms_inhibit_state(u, PA_SOURCE_IS_OPENED(state), NULL, s);
+    }
+
+    return PA_HOOK_OK;
+}
+
+static void dpms_inhibit_info_free(struct dpms_inhibit_info *d) {
+    pa_assert(d);
+
+    if (d->source)
+        pa_source_unref(d->source);
+    if (d->sink)
+        pa_sink_unref(d->sink);
+
+    pa_xfree(d);
+}
+
+int pa__init(pa_module*m) {
+    pa_modargs *ma = NULL;
+    struct userdata *u;
+    uint32_t idx;
+    pa_sink *sink;
+    pa_source *source;
+
+    pa_assert(m);
+
+    if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
+        pa_log("Failed to parse module arguments.");
+        goto fail;
+    }
+
+    m->userdata = u = pa_xnew(struct userdata, 1);
+    u->core = m->core;
+    u->inhibit_infos = pa_hashmap_new_full(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func, NULL, (pa_free_cb_t) dpms_inhibit_info_free);
+
+    PA_IDXSET_FOREACH(sink, m->core->sinks, idx)
+        device_recheck_dpms_inhibit_state(m->core, PA_OBJECT(sink), u);
+
+    PA_IDXSET_FOREACH(source, m->core->sources, idx)
+        device_recheck_dpms_inhibit_state(m->core, PA_OBJECT(source), u);
+
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_UNLINK_POST], PA_HOOK_NORMAL, (pa_hook_cb_t) device_unlink_hook_cb, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_UNLINK_POST], PA_HOOK_NORMAL, (pa_hook_cb_t) device_unlink_hook_cb, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_STATE_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_STATE_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_PORT_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_PORT_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_PROPLIST_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_PROPLIST_CHANGED], PA_HOOK_NORMAL, (pa_hook_cb_t) device_recheck_dpms_inhibit_state, u);
+    /* There is no PA_CORE_HOOK_PORT_PROPLIST_CHANGED */
+
+    pa_modargs_free(ma);
+    return 0;
+
+fail:
+
+    if (ma)
+        pa_modargs_free(ma);
+
+    return -1;
+}
+
+void pa__done(pa_module*m) {
+    struct userdata *u;
+    struct dpms_inhibit_info *d;
+    void *state;
+
+    pa_assert(m);
+
+    if (!m->userdata)
+        return;
+
+    u = m->userdata;
+
+    PA_HASHMAP_FOREACH(d, u->inhibit_infos, state) {
+        if (d->sink) {
+            pa_log_info("Uninhibiting DPMS off for sink %s on module unload.", d->sink->name);
+        }
+
+        if (d->source) {
+            pa_log_info("Uninhibiting DPMS off for source %s on module unload.", d->source->name);
+        }
+    }
+
+    pa_hashmap_free(u->inhibit_infos);
+
+    pa_xfree(u);
+}
diff --git a/src/pulse/proplist.h b/src/pulse/proplist.h
index cec3b357..a5040db9 100644
--- a/src/pulse/proplist.h
+++ b/src/pulse/proplist.h
@@ -208,6 +208,9 @@  PA_C_DECL_BEGIN
 /** For devices: device class. One of "sound", "modem", "monitor", "filter" */
 #define PA_PROP_DEVICE_CLASS                   "device.class"
 
+/** For devices: Whether DPMS OFF must not happen while this sink is in use. */
+#define PA_PROP_DEVICE_REQUIRES_NO_DPMS_OFF    "device.requires.no_dpms_off"
+
 /** For devices: form factor if applicable. One of "internal", "speaker", "handset", "tv", "webcam", "microphone", "headset", "headphone", "hands-free", "car", "hifi", "computer", "portable" */
 #define PA_PROP_DEVICE_FORM_FACTOR             "device.form_factor"