[pulseaudio] Fix sink-input configuration and other (#11272) (#11276)

* [pulseaudio] Fix sink-input configuration  and other small improvements (#11272)

The binding requires a parameter to activate the parsing of sink-input entries on the pulseaudio server. This patch :
- document this behaviour
- fix the parsing of these parameters if a configuration file is used (the old method of casting launched a class cast exception)

Other small improvements :
- Force a refresh/new parsing when the configuration changes
- Fix scheduled disconnection : if a sound is played during the grace period, the scheduled disconnection is postponed, not added to the last
- add a possibility to never disconnect the audio sink (in order to have a lower latency when playing sound)
Closes #11272

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

* Small fixes after proofreading

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>

Co-authored-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
dalgwen
2021-10-09 11:44:20 +02:00
committed by GitHub
parent f3fbcb622f
commit 83fd01498a
10 changed files with 165 additions and 51 deletions

View File

@@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
@@ -66,6 +67,8 @@ public class PulseAudioAudioSink implements AudioSink {
private boolean isIdle = true;
private @Nullable ScheduledFuture<?> scheduledDisconnection;
static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
@@ -254,8 +257,14 @@ public class PulseAudioAudioSink implements AudioSink {
}
public void scheduleDisconnect() {
logger.debug("Scheduling disconnect");
scheduler.schedule(this::disconnect, pulseaudioHandler.getIdleTimeout(), TimeUnit.MILLISECONDS);
if (scheduledDisconnection != null) {
scheduledDisconnection.cancel(true);
}
int idleTimeout = pulseaudioHandler.getIdleTimeout();
if (idleTimeout > -1) {
logger.debug("Scheduling disconnect");
scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
}
}
@Override

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains the binding configuration
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public class PulseAudioBindingConfiguration {
public boolean sink = true;
public boolean source = false;
public boolean sinkInput = false;
public boolean sourceOutput = false;
private Set<PulseAudioBindingConfigurationListener> listeners = new HashSet<>();
public void addPulseAudioBindingConfigurationListener(PulseAudioBindingConfigurationListener listener) {
listeners.add(listener);
}
public void removePulseAudioBindingConfigurationListener(PulseAudioBindingConfigurationListener listener) {
listeners.remove(listener);
}
public void update(PulseAudioBindingConfiguration newConfiguration) {
sink = newConfiguration.sink;
source = newConfiguration.source;
sinkInput = newConfiguration.sinkInput;
sourceOutput = newConfiguration.sourceOutput;
listeners.forEach(PulseAudioBindingConfigurationListener::bindingConfigurationChanged);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for listening to configuration change
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public interface PulseAudioBindingConfigurationListener {
public void bindingConfigurationChanged();
}

View File

@@ -12,9 +12,6 @@
*/
package org.openhab.binding.pulseaudio.internal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@@ -57,13 +54,4 @@ public class PulseaudioBindingConstants {
public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;
public static final Map<String, Boolean> TYPE_FILTERS = new HashMap<>();
static {
TYPE_FILTERS.put(SINK_THING_TYPE.getId(), true);
TYPE_FILTERS.put(SINK_INPUT_THING_TYPE.getId(), false);
TYPE_FILTERS.put(SOURCE_THING_TYPE.getId(), false);
TYPE_FILTERS.put(SOURCE_OUTPUT_THING_TYPE.getId(), false);
}
}

View File

@@ -59,6 +59,11 @@ public class PulseaudioClient {
private List<AbstractAudioDeviceConfig> items;
private List<Module> modules;
/**
* Corresponding to the global binding configuration
*/
private PulseAudioBindingConfiguration configuration;
/**
* corresponding name to execute actions on sink items
*/
@@ -119,13 +124,10 @@ public class PulseaudioClient {
*/
private static final String MODULE_COMBINE_SINK = "module-combine-sink";
public PulseaudioClient() throws IOException {
this("localhost", 4712);
}
public PulseaudioClient(String host, int port) throws IOException {
public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) throws IOException {
this.host = host;
this.port = port;
this.configuration = configuration;
items = new ArrayList<>();
modules = new ArrayList<>();
@@ -147,19 +149,19 @@ public class PulseaudioClient {
List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
newItems.clear();
if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) {
if (configuration.sink) {
logger.debug("reading sinks");
newItems.addAll(Parser.parseSinks(listSinks(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) {
if (configuration.source) {
logger.debug("reading sources");
newItems.addAll(Parser.parseSources(listSources(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) {
if (configuration.sinkInput) {
logger.debug("reading sink-inputs");
newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) {
if (configuration.sourceOutput) {
logger.debug("reading source-outputs");
newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
}

View File

@@ -13,8 +13,6 @@
package org.openhab.binding.pulseaudio.internal;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
@@ -36,7 +34,9 @@ import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +56,8 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
private final Map<ThingHandler, ServiceRegistration<?>> discoveryServiceReg = new HashMap<>();
private PulseAudioBindingConfiguration configuration = new PulseAudioBindingConfiguration();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@@ -109,7 +111,7 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing);
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing, configuration);
registerDeviceDiscoveryService(handler);
return handler;
} else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
@@ -119,25 +121,16 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
return null;
}
@Override
protected synchronized void activate(ComponentContext componentContext) {
// The activate component call is used to access the bindings configuration
@Activate
protected synchronized void activate(ComponentContext componentContext, Map<String, Object> config) {
super.activate(componentContext);
modified(componentContext);
modified(config);
}
protected synchronized void modified(ComponentContext componentContext) {
Dictionary<String, ?> properties = componentContext.getProperties();
logger.info("pulseaudio configuration update received ({})", properties);
if (properties == null) {
return;
}
Enumeration<String> e = properties.keys();
while (e.hasMoreElements()) {
String k = e.nextElement();
if (PulseaudioBindingConstants.TYPE_FILTERS.containsKey(k)) {
PulseaudioBindingConstants.TYPE_FILTERS.put(k, (boolean) properties.get(k));
}
logger.debug("update received {}: {}", k, properties.get(k));
}
@Modified
protected void modified(Map<String, Object> config) {
configuration.update(new Configuration(config).as(PulseAudioBindingConfiguration.class));
logger.debug("pulseaudio configuration update received ({})", config);
}
}

View File

@@ -24,6 +24,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfiguration;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfigurationListener;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
@@ -45,7 +47,7 @@ import org.slf4j.LoggerFactory;
* @author Tobias Bräutigam - Initial contribution
*
*/
public class PulseaudioBridgeHandler extends BaseBridgeHandler {
public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
@@ -58,11 +60,17 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
private PulseaudioClient client;
private PulseAudioBindingConfiguration configuration;
private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
private HashSet<String> lastActiveDevices = new HashSet<>();
private ScheduledFuture<?> pollingJob;
private Runnable pollingRunnable = () -> {
update();
};
private synchronized void update() {
client.update();
for (AbstractAudioDeviceConfig device : client.getItems()) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
@@ -85,10 +93,11 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
}
}
}
};
}
public PulseaudioBridgeHandler(Bridge bridge) {
public PulseaudioBridgeHandler(Bridge bridge, PulseAudioBindingConfiguration configuration) {
super(bridge);
this.configuration = configuration;
}
@Override
@@ -132,7 +141,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
if (host != null && !host.isEmpty()) {
Runnable connectRunnable = () -> {
try {
client = new PulseaudioClient(host, port);
client = new PulseaudioClient(host, port, configuration);
if (client.isConnected()) {
updateStatus(ThingStatus.ONLINE);
logger.info("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
@@ -151,10 +160,13 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
host, port);
updateStatus(ThingStatus.OFFLINE);
}
this.configuration.addPulseAudioBindingConfigurationListener(this);
}
@Override
public void dispose() {
this.configuration.removePulseAudioBindingConfigurationListener(this);
if (pollingJob != null) {
pollingJob.cancel(true);
}
@@ -174,4 +186,9 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
return deviceStatusListeners.remove(deviceStatusListener);
}
@Override
public void bindingConfigurationChanged() {
update();
}
}

View File

@@ -146,8 +146,10 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
refreshJob = null;
}
updateStatus(ThingStatus.OFFLINE);
bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
if (bridgeHandler != null) {
bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
}
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();

View File

@@ -36,7 +36,7 @@
<parameter name="simpleProtocolSinkIdleTimeout" type="integer" required="false">
<label>Idle Timeout</label>
<description>Timeout in ms after which the connection will be closed when no stream is running. This ensures that
your speaker is not on all the time and the pulseaudio sink can go to idle mode.
your speaker is not on all the time and the pulseaudio sink can go to idle mode. -1 for no disconnection.
</description>
<default>30000</default>
</parameter>