From f7396ac712b3b824ca3ae97b4a5fb717354be733 Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Fri, 2 Apr 2021 15:29:50 +0200 Subject: [PATCH 1/5] [pulseaudio] Add pulseaudio sink as openhab audio sink (#1895) This add to the pulseaudio binding the capability to use "pulseaudio sink" as an "openhab sink" to output sound from openhab to a pulse audio server on the network. You need to load module-simple-protocol-tcp sink in addition to the usual module-cli-protocol-tcp, and enable the sink in the thing configuration. Closes #1895 Signed-off-by: Gwendal Roulleau --- .../org.openhab.binding.pulseaudio/README.md | 8 +- .../org.openhab.binding.pulseaudio/pom.xml | 21 ++ .../src/main/feature/feature.xml | 3 + .../internal/PulseAudioAudioSink.java | 188 ++++++++++++++++++ .../internal/PulseaudioBindingConstants.java | 2 + .../internal/PulseaudioHandlerFactory.java | 6 +- .../handler/PulseaudioBridgeHandler.java | 4 +- .../internal/handler/PulseaudioHandler.java | 97 ++++++++- .../src/main/resources/OH-INF/thing/sink.xml | 12 ++ 9 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java diff --git a/bundles/org.openhab.binding.pulseaudio/README.md b/bundles/org.openhab.binding.pulseaudio/README.md index 39b7766620dfb..b130c3482045c 100644 --- a/bundles/org.openhab.binding.pulseaudio/README.md +++ b/bundles/org.openhab.binding.pulseaudio/README.md @@ -6,7 +6,7 @@ This binding integrates pulseaudio devices. The Pulseaudio bridge is required as a "bridge" for accessing any other Pulseaudio devices. -You need a running pulseaudio server whith module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported: +You need a running pulseaudio server with module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported: * Sink * Source @@ -35,6 +35,12 @@ All devices support some of the following channels: | slaves | String | Slave sinks of a combined sink | | routeToSink | String | Shows the sink a sink-input is currently routed to | +## Audio sink + +Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported. +Use the appropriate parameter in the sink thing to activate this possibility. +This requires the module **module-simple-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. + ## Full Example ### pulseaudio.things ``` diff --git a/bundles/org.openhab.binding.pulseaudio/pom.xml b/bundles/org.openhab.binding.pulseaudio/pom.xml index 29ec07a21c6e3..a7e816c165d66 100644 --- a/bundles/org.openhab.binding.pulseaudio/pom.xml +++ b/bundles/org.openhab.binding.pulseaudio/pom.xml @@ -14,4 +14,25 @@ openHAB Add-ons :: Bundles :: Pulseaudio Binding + + + com.googlecode.soundlibs + mp3spi + 1.9.5.4 + compile + + + com.googlecode.soundlibs + jlayer + 1.0.1.4 + compile + + + com.googlecode.soundlibs + tritonus-share + 0.3.7.4 + compile + + + diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml b/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml index afdd623480728..cd43aea257f86 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/feature/feature.xml @@ -7,5 +7,8 @@ openhab-transport-mdns openhab-transport-upnp mvn:org.openhab.addons.bundles/org.openhab.binding.pulseaudio/${project.version} + mvn:com.googlecode.soundlibs/tritonus-share/0.3.7.4 + mvn:com.googlecode.soundlibs/mp3spi/1.9.5.4 + mvn:com.googlecode.soundlibs/jlayer/1.0.1.4 diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java new file mode 100644 index 0000000000000..8a9974d706937 --- /dev/null +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -0,0 +1,188 @@ +/** + * 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.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; +import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioSink; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.audio.FixedLengthAudioStream; +import org.openhab.core.audio.UnsupportedAudioFormatException; +import org.openhab.core.audio.UnsupportedAudioStreamException; +import org.openhab.core.library.types.PercentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The audio sink for openhab, implemented by a connection to a pulseaudio sink + * + * @author Gwendal Roulleau - Initial contribution + * + */ +public class PulseAudioAudioSink implements AudioSink { + + private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class); + + private static final HashSet SUPPORTED_FORMATS = new HashSet<>(); + private static final HashSet> SUPPORTED_STREAMS = new HashSet<>(); + + private PulseaudioHandler pulseaudioHandler; + + private Socket clientSocket; + + static { + SUPPORTED_FORMATS.add(AudioFormat.WAV); + SUPPORTED_FORMATS.add(AudioFormat.MP3); + SUPPORTED_STREAMS.add(FixedLengthAudioStream.class); + } + + public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) { + this.pulseaudioHandler = pulseaudioHandler; + } + + @Override + public String getId() { + return pulseaudioHandler.getThing().getUID().toString(); + } + + @Override + public @Nullable String getLabel(@Nullable Locale locale) { + return pulseaudioHandler.getThing().getLabel(); + } + + /** + * Convert MP3 to PCM, as this is the only possible format + * + * @param input + * @return + */ + private InputStream getPCMStreamFromMp3Stream(InputStream input) { + try { + MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader(); + AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input); + javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat(); + + MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider(); + javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat( + javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16, + sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false); + + return mpegconverter.getAudioInputStream(convertFormat, sourceAIS); + + } catch (IOException | UnsupportedAudioFileException e) { + logger.error("Cannot convert this mp3 stream to pcm stream", e); + } + return null; + } + + /** + * Connect to pulseaudio with the simple protocol + * + * @throws UnknownHostException + * @throws IOException + */ + private void connectIfNeeded() throws IOException { + if (clientSocket == null || !clientSocket.isConnected() || clientSocket.isClosed()) { + String host = pulseaudioHandler.getHost(); + int port = pulseaudioHandler.getSimpleTcpPort(); + clientSocket = new Socket(host, port); + clientSocket.setSoTimeout(500); + } + } + + /** + * Disconnect the socket to pulseaudio simple protocol + */ + public void disconnect() { + if (clientSocket == null) { + try { + clientSocket.close(); + } catch (IOException e) { + } + } + } + + @Override + public void process(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + + if (audioStream == null) { + return; + } + + InputStream audioInputStream = null; + try { + + if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) { + audioInputStream = getPCMStreamFromMp3Stream(audioStream); + } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) { + audioInputStream = audioStream; + } else { + throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream", + audioStream.getFormat()); + } + + try { + connectIfNeeded(); + // send raw audio to the socket and to pulse audio + audioInputStream.transferTo(clientSocket.getOutputStream()); + } catch (IOException e) { + logger.error("Error while trying to send audio to pulseaudio audio sink. Cannot connect to {} : {}", + pulseaudioHandler.getHost(), pulseaudioHandler.getSimpleTcpPort()); + } + } finally { + try { + if (audioInputStream != null) { + audioInputStream.close(); + } + ; + audioStream.close(); + } catch (IOException e) { + } + } + } + + @Override + public Set getSupportedFormats() { + return SUPPORTED_FORMATS; + } + + @Override + public Set> getSupportedStreams() { + return SUPPORTED_STREAMS; + } + + @Override + public PercentType getVolume() { + return new PercentType(pulseaudioHandler.getLastVolume()); + } + + @Override + public void setVolume(PercentType volume) { + pulseaudioHandler.setVolume(volume.intValue()); + } +} diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java index 19d17c9fec0b8..669697021e90d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java @@ -51,6 +51,8 @@ public class PulseaudioBindingConstants { public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh"; public static final String DEVICE_PARAMETER_NAME = "name"; + public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink"; + public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort"; public static final Map TYPE_FILTERS = new HashMap<>(); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java index 4afffd91b7652..771efc9fdacf3 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java @@ -99,18 +99,22 @@ protected void removeHandler(ThingHandler thingHandler) { discoveryServiceReg.get(thingHandler).unregister(); discoveryServiceReg.remove(thingHandler); } + super.removeHandler(thingHandler); } @Override protected ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing); registerDeviceDiscoveryService(handler); return handler; } else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new PulseaudioHandler(thing); + PulseaudioHandler pulseaudioHandler = new PulseaudioHandler(thing, bundleContext); + return pulseaudioHandler; } return null; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java index 12d56ad7d96a5..ec4f104c18bf4 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java @@ -155,7 +155,9 @@ public void initialize() { @Override public void dispose() { - pollingJob.cancel(true); + if (pollingJob != null) { + pollingJob.cancel(true); + } client.disconnect(); super.dispose(); } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index 1b8f1d63a52a0..d5161735c87c3 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -14,18 +14,25 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; +import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink; +import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.Sink; import org.openhab.binding.pulseaudio.internal.items.SinkInput; +import org.openhab.core.audio.AudioSink; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; @@ -44,6 +51,8 @@ import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,8 +77,17 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL private String name; - public PulseaudioHandler(Thing thing) { + private PulseAudioAudioSink audioSink; + + private Integer savedVolume; + + private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); + + private BundleContext bundleContext; + + public PulseaudioHandler(Thing thing, BundleContext bundleContext) { super(thing); + this.bundleContext = bundleContext; } @Override @@ -80,6 +98,23 @@ public void initialize() { // until we get an update put the Thing offline updateStatus(ThingStatus.OFFLINE); deviceOnlineWatchdog(); + + // if it's a SINK thing, then maybe we have to activate the audio sink + if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) { + // check the property to see if we it's enabled : + Boolean sinkActivated = (Boolean) thing.getConfiguration() + .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION); + if (sinkActivated != null && sinkActivated) { + // Register the sink as an audio sink in openhab + logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID()); + PulseAudioAudioSink audioSink = new PulseAudioAudioSink(this); + setAudioSink(audioSink); + @SuppressWarnings("unchecked") + ServiceRegistration reg = (ServiceRegistration) bundleContext + .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>()); + audioSinkRegistrations.put(thing.getUID().toString(), reg); + } + } } @Override @@ -92,6 +127,13 @@ public void dispose() { bridgeHandler = null; logger.trace("Thing {} {} disposed.", getThing().getUID(), name); super.dispose(); + + // Unregister the potential pulse audio sink's audio sink + ServiceRegistration reg = audioSinkRegistrations.get(getThing().getUID().toString()); + if (reg != null) { + logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID()); + reg.unregister(); + } } private void deviceOnlineWatchdog() { @@ -162,15 +204,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { // refresh to get the current volume level bridge.getClient().update(); device = bridge.getDevice(name); - int volume = device.getVolume(); + savedVolume = device.getVolume(); if (command.equals(IncreaseDecreaseType.INCREASE)) { - volume = Math.min(100, volume + 5); + savedVolume = Math.min(100, savedVolume + 5); } if (command.equals(IncreaseDecreaseType.DECREASE)) { - volume = Math.max(0, volume - 5); + savedVolume = Math.max(0, savedVolume - 5); } - bridge.getClient().setVolumePercent(device, volume); - updateState = new PercentType(volume); + bridge.getClient().setVolumePercent(device, savedVolume); + updateState = new PercentType(savedVolume); } else if (command instanceof PercentType) { DecimalType volume = (DecimalType) command; bridge.getClient().setVolumePercent(device, volume.intValue()); @@ -227,12 +269,37 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } + /** + * Use last checked volume for faster access + * + * @return + */ + public int getLastVolume() { + if (savedVolume == null) { + PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridge.getDevice(name); + // refresh to get the current volume level + bridge.getClient().update(); + device = bridge.getDevice(name); + savedVolume = device.getVolume(); + } + return savedVolume == null ? 50 : savedVolume; + } + + public void setVolume(int volume) { + PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridge.getDevice(name); + bridge.getClient().setVolumePercent(device, volume); + updateState(VOLUME_CHANNEL, new PercentType(volume)); + } + @Override public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) { if (device.getPaName().equals(name)) { updateStatus(ThingStatus.ONLINE); logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL); - updateState(VOLUME_CHANNEL, new PercentType(device.getVolume())); + savedVolume = device.getVolume(); + updateState(VOLUME_CHANNEL, new PercentType(savedVolume)); updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF); updateState(STATE_CHANNEL, device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-")); @@ -248,11 +315,23 @@ public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig devi } } + @SuppressWarnings("null") + public String getHost() { + return (String) getBridge().getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); + } + + public int getSimpleTcpPort() { + return ((BigDecimal) getThing().getConfiguration() + .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue(); + } + @Override public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) { if (device.getPaName().equals(name)) { bridgeHandler.unregisterDeviceStatusListener(this); bridgeHandler = null; + audioSink.disconnect(); + audioSink = null; updateStatus(ThingStatus.OFFLINE); } } @@ -261,4 +340,8 @@ public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceC public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) { logger.trace("new device discovered {} by {}", device, bridge); } + + public void setAudioSink(PulseAudioAudioSink audioSink) { + this.audioSink = audioSink; + } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml index 75a2eb06ca95e..40d7b257dc4c0 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml @@ -21,6 +21,18 @@ The name of one specific device. + + + Activation of a corresponding sink in openHAB (needs module-simple-protocol-tcp loaded on the server) + false + false + + + + Port of the module-simple-protocol-tcp listening for this sink + 4711 + false + From 05471b4b2614d7bbb795eca3894d94bec08aa6aa Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Fri, 2 Apr 2021 23:54:19 +0200 Subject: [PATCH 2/5] Small corrections after review And getting rid of some other compilation warnings Signed-off-by: Gwendal Roulleau --- .../org.openhab.binding.pulseaudio/pom.xml | 2 +- .../internal/PulseAudioAudioSink.java | 27 +++++++++++-------- .../pulseaudio/internal/PulseaudioClient.java | 16 ++++++----- .../internal/PulseaudioHandlerFactory.java | 13 +++++---- .../pulseaudio/internal/cli/Parser.java | 15 ++++++----- .../PulseaudioDiscoveryParticipant.java | 1 - .../internal/handler/PulseaudioHandler.java | 7 ++++- .../src/main/resources/OH-INF/thing/sink.xml | 8 +++--- 8 files changed, 50 insertions(+), 39 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/pom.xml b/bundles/org.openhab.binding.pulseaudio/pom.xml index a7e816c165d66..efc42fa11186b 100644 --- a/bundles/org.openhab.binding.pulseaudio/pom.xml +++ b/bundles/org.openhab.binding.pulseaudio/pom.xml @@ -18,7 +18,7 @@ com.googlecode.soundlibs mp3spi - 1.9.5.4 + 1.9.5.4 compile diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java index 8a9974d706937..1a881860c83b0 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -19,12 +19,11 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; -import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; -import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.UnsupportedAudioFileException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; import org.openhab.core.audio.AudioFormat; @@ -37,12 +36,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; +import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; + /** * The audio sink for openhab, implemented by a connection to a pulseaudio sink * * @author Gwendal Roulleau - Initial contribution * */ +@NonNullByDefault public class PulseAudioAudioSink implements AudioSink { private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class); @@ -52,7 +55,7 @@ public class PulseAudioAudioSink implements AudioSink { private PulseaudioHandler pulseaudioHandler; - private Socket clientSocket; + private @Nullable Socket clientSocket; static { SUPPORTED_FORMATS.add(AudioFormat.WAV); @@ -80,7 +83,7 @@ public String getId() { * @param input * @return */ - private InputStream getPCMStreamFromMp3Stream(InputStream input) { + private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) { try { MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader(); AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input); @@ -94,7 +97,7 @@ private InputStream getPCMStreamFromMp3Stream(InputStream input) { return mpegconverter.getAudioInputStream(convertFormat, sourceAIS); } catch (IOException | UnsupportedAudioFileException e) { - logger.error("Cannot convert this mp3 stream to pcm stream", e); + logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage()); } return null; } @@ -106,7 +109,8 @@ private InputStream getPCMStreamFromMp3Stream(InputStream input) { * @throws IOException */ private void connectIfNeeded() throws IOException { - if (clientSocket == null || !clientSocket.isConnected() || clientSocket.isClosed()) { + Socket clientSocketLocal = clientSocket; + if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { String host = pulseaudioHandler.getHost(); int port = pulseaudioHandler.getSimpleTcpPort(); clientSocket = new Socket(host, port); @@ -118,7 +122,7 @@ private void connectIfNeeded() throws IOException { * Disconnect the socket to pulseaudio simple protocol */ public void disconnect() { - if (clientSocket == null) { + if (clientSocket != null) { try { clientSocket.close(); } catch (IOException e) { @@ -148,10 +152,12 @@ public void process(@Nullable AudioStream audioStream) try { connectIfNeeded(); - // send raw audio to the socket and to pulse audio - audioInputStream.transferTo(clientSocket.getOutputStream()); + if (audioInputStream != null && clientSocket != null) { + // send raw audio to the socket and to pulse audio + audioInputStream.transferTo(clientSocket.getOutputStream()); + } } catch (IOException e) { - logger.error("Error while trying to send audio to pulseaudio audio sink. Cannot connect to {} : {}", + logger.warn("Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}", pulseaudioHandler.getHost(), pulseaudioHandler.getSimpleTcpPort()); } } finally { @@ -159,7 +165,6 @@ public void process(@Nullable AudioStream audioStream) if (audioInputStream != null) { audioInputStream.close(); } - ; audioStream.close(); } catch (IOException e) { } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 80c61f6fae230..779ea0bb30d58 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -24,6 +24,7 @@ import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.openhab.binding.pulseaudio.internal.cli.Parser; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; @@ -143,19 +144,19 @@ public void update() { modules.addAll(Parser.parseModules(listModules())); items.clear(); - if (TYPE_FILTERS.get(SINK_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sinks"); items.addAll(Parser.parseSinks(listSinks(), this)); } - if (TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sources"); items.addAll(Parser.parseSources(listSources(), this)); } - if (TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sink-inputs"); items.addAll(Parser.parseSinkInputs(listSinkInputs(), this)); } - if (TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())) { + if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading source-outputs"); items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this)); } @@ -404,13 +405,14 @@ private String getItemCommandName(AbstractAudioDeviceConfig item) { * values from 0 - 100) */ public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) { + int volumeToSet = vol; if (item == null) { return; } - if (vol <= 100) { - vol = toAbsoluteVolume(vol); + if (volumeToSet <= 100) { + volumeToSet = toAbsoluteVolume(volumeToSet); } - setVolume(item, vol); + setVolume(item, volumeToSet); } /** diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java index 771efc9fdacf3..06c32ab26605d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java @@ -92,14 +92,14 @@ private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, ThingUID thin @Override protected void removeHandler(ThingHandler thingHandler) { - if (this.discoveryServiceReg.containsKey(thingHandler)) { + ServiceRegistration serviceRegistration = this.discoveryServiceReg.get(thingHandler); + if (serviceRegistration != null) { PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext - .getService(discoveryServiceReg.get(thingHandler).getReference()); + .getService(serviceRegistration.getReference()); service.deactivate(); - discoveryServiceReg.get(thingHandler).unregister(); - discoveryServiceReg.remove(thingHandler); + serviceRegistration.unregister(); } - + discoveryServiceReg.remove(thingHandler); super.removeHandler(thingHandler); } @@ -113,8 +113,7 @@ protected ThingHandler createHandler(Thing thing) { registerDeviceDiscoveryService(handler); return handler; } else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - PulseaudioHandler pulseaudioHandler = new PulseaudioHandler(thing, bundleContext); - return pulseaudioHandler; + return new PulseaudioHandler(thing, bundleContext); } return null; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java index 68905acc44c39..9675d4400f790 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java @@ -135,15 +135,18 @@ public static Collection parseSinks(String raw, PulseaudioClient client) { } } if (properties.containsKey("muted")) { - sink.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + sink.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); } if (properties.containsKey("combine.slaves")) { // this is a combined sink, the combined sink object should be - for (String sinkName : properties.get("combine.slaves").replace("\"", "").split(",")) { - sink.addCombinedSinkName(sinkName); + String sinkNames = properties.get("combine.slaves"); + if (sinkNames != null) { + for (String sinkName : sinkNames.replace("\"", "").split(",")) { + sink.addCombinedSinkName(sinkName); + } } combinedSinks.add(sink); } @@ -203,7 +206,7 @@ public static List parseSinkInputs(String raw, PulseaudioClient clien } } if (properties.containsKey("muted")) { - item.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + item.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { item.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); @@ -262,7 +265,7 @@ public static List parseSources(String raw, PulseaudioClient client) { } } if (properties.containsKey("muted")) { - source.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + source.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { source.setVolume(parseVolume(properties.get("volume"))); @@ -322,7 +325,7 @@ public static List parseSourceOutputs(String raw, PulseaudioClient } } if (properties.containsKey("muted")) { - item.setMuted(properties.get("muted").equalsIgnoreCase("yes")); + item.setMuted("yes".equalsIgnoreCase(properties.get("muted"))); } if (properties.containsKey("volume")) { item.setVolume(Integer.valueOf(parseVolume(properties.get("volume")))); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java index 86ffa16c99bda..3a7a24fe3891c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java @@ -73,7 +73,6 @@ public DiscoveryResult createResult(ServiceInfo info) { } return result; } catch (IOException e) { - result = null; } } return result; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index d5161735c87c3..d1c763e713829 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -317,7 +317,12 @@ public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig devi @SuppressWarnings("null") public String getHost() { - return (String) getBridge().getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); + if (getBridge() != null) { + return (String) getBridge().getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); + } else { + logger.error("A bridge must be configured for this pulseaudio thing"); + return "null"; + } } public int getSimpleTcpPort() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml index 40d7b257dc4c0..2e014afdf2058 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml @@ -21,17 +21,15 @@ The name of one specific device. - + Activation of a corresponding sink in openHAB (needs module-simple-protocol-tcp loaded on the server) false - false - - + + Port of the module-simple-protocol-tcp listening for this sink 4711 - false From 9fb5a199da963c41535eaea08588e3fd1d614071 Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Sun, 4 Apr 2021 01:37:40 +0200 Subject: [PATCH 3/5] Fix some registration errors and allow the binding to load the simple module remotely Signed-off-by: Gwendal Roulleau --- .../org.openhab.binding.pulseaudio/README.md | 6 +- .../internal/PulseAudioAudioSink.java | 29 +++++-- .../internal/PulseaudioBindingConstants.java | 3 + .../pulseaudio/internal/PulseaudioClient.java | 77 ++++++++++++++++++- .../internal/handler/PulseaudioHandler.java | 27 ++++++- .../src/main/resources/OH-INF/thing/sink.xml | 8 +- 6 files changed, 134 insertions(+), 16 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/README.md b/bundles/org.openhab.binding.pulseaudio/README.md index b130c3482045c..8951f9d18b872 100644 --- a/bundles/org.openhab.binding.pulseaudio/README.md +++ b/bundles/org.openhab.binding.pulseaudio/README.md @@ -38,15 +38,15 @@ All devices support some of the following channels: ## Audio sink Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported. -Use the appropriate parameter in the sink thing to activate this possibility. -This requires the module **module-simple-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. +Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink). +This requires the module **module-simple-protocol-tcp** to be present on the server which runs your openHAB instance. The binding will try to command (if not discovered first) the load of this module on the pulseaudio server. ## Full Example ### pulseaudio.things ``` Bridge pulseaudio:bridge: "" @ "" [ host="", port=4712 ] { Things: - Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3"] // this name corresponds to pactl list-sinks output + Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the name corresponds to pactl list-sinks output Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"] Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"] Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"] diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java index 1a881860c83b0..93ae3baa9246c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -19,6 +19,8 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; +import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.UnsupportedAudioFileException; @@ -36,9 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; -import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; - /** * The audio sink for openhab, implemented by a connection to a pulseaudio sink * @@ -108,7 +107,7 @@ public String getId() { * @throws UnknownHostException * @throws IOException */ - private void connectIfNeeded() throws IOException { + public void connectIfNeeded() throws IOException { Socket clientSocketLocal = clientSocket; if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { String host = pulseaudioHandler.getHost(); @@ -156,9 +155,25 @@ public void process(@Nullable AudioStream audioStream) // send raw audio to the socket and to pulse audio audioInputStream.transferTo(clientSocket.getOutputStream()); } - } catch (IOException e) { - logger.warn("Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}", - pulseaudioHandler.getHost(), pulseaudioHandler.getSimpleTcpPort()); + } catch (IOException e) { // second try, as the socket disconnection is sometimes not well detected + disconnect(); + try { + connectIfNeeded(); + if (audioInputStream != null && clientSocket != null) { + // send raw audio to the socket and to pulse audio + audioInputStream.transferTo(clientSocket.getOutputStream()); + } + } catch (IOException e1) { + if (clientSocket != null) { + logger.warn( + "Error while trying to send audio to pulseaudio audio sink. Cannot connect to port {} (ip {}), error: {}", + clientSocket.getPort(), pulseaudioHandler.getHost(), e1.getMessage()); + } else { + logger.warn( + "Error while trying to send audio to pulseaudio audio sink. Cannot connect. (error: {})", + e1.getMessage()); + } + } } } finally { try { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java index 669697021e90d..9c8f4f8521927 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java @@ -54,6 +54,9 @@ public class PulseaudioBindingConstants { public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink"; public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort"; + 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 TYPE_FILTERS = new HashMap<>(); static { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 779ea0bb30d58..d5249b6aa0172 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -25,7 +25,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Random; +import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.pulseaudio.internal.cli.Parser; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State; @@ -378,6 +380,77 @@ public void setVolume(AbstractAudioDeviceConfig item, int vol) { item.setVolume(Math.round(100f / 65536f * vol)); } + /** + * Locate or load (if needed) the simple protocol tcp module for the given sink + * and returns the port. + * The module loading (if needed) will be tried several times, on a new random port each time. + * + * @param item the sink we are searching for + * @param simpleTcpPortPref the port to use if we have to load the module + * @return the port on which the module is listening + */ + public Optional loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item, + Integer simpleTcpPortPref) { + int currentTry = 0; + int simpleTcpPortToTry = simpleTcpPortPref; + do { + Optional simplePort = findSimpleProtocolTcpModule(item); + + if (simplePort.isPresent()) { + return simplePort; + } else { + sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port=" + + simpleTcpPortToTry); + simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024 + } + try { // let time for the module to load + Thread.sleep(100); + } catch (InterruptedException e) { + } + + currentTry++; + } while (currentTry < 3); + + logger.warn("The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp" + + " on random port on the pulseaudio server and give up trying"); + return Optional.empty(); + } + + /** + * Find a simple protocol module corresponding to the given sink in argument + * and returns the port it listens to + * + * @param item + * @return + */ + private Optional findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) { + update(); + + List modulesCopy = new ArrayList(modules); + return modulesCopy.stream() // iteration on modules + .filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name + .filter(module -> extractArgumentFromLine("sink", module.getArgument()) // extract sink in argument + .map(sinkName -> sinkName.equals(item.getPaName())).orElse(false)) // filter on sink name + .findAny() // get a corresponding module + .map(module -> extractArgumentFromLine("port", module.getArgument()) + .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port + .map(portS -> Integer.parseInt(portS)); + } + + private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) { + String argument = null; + int startPortIndex = argumentLine.indexOf(argumentWanted + "="); + if (startPortIndex != -1) { + startPortIndex = startPortIndex + argumentWanted.length() + 1; + int endPortIndex = argumentLine.indexOf(" ", startPortIndex); + if (endPortIndex == -1) { + endPortIndex = argumentLine.length(); + } + argument = argumentLine.substring(startPortIndex, endPortIndex); + } + return Optional.ofNullable(argument); + } + /** * returns the item names that can be used in commands * @@ -585,6 +658,8 @@ private String sendRawRequest(String command) { } catch (SocketTimeoutException e) { // Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance // to detect the end of the answer, except by this timeout + } catch (SocketException e) { + logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage()); } catch (IOException e) { logger.error("Exception while reading socket: {}", e.getMessage()); } @@ -621,7 +696,7 @@ private void connect() throws IOException { } catch (NoRouteToHostException e) { logger.error("no route to host {}", host); } catch (SocketException e) { - logger.error("{}", e.getLocalizedMessage(), e); + logger.error("cannot connect to host {} : {}", host, e.getMessage()); } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index d1c763e713829..3da3ca8e7f65e 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*; +import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; @@ -109,6 +110,12 @@ public void initialize() { logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID()); PulseAudioAudioSink audioSink = new PulseAudioAudioSink(this); setAudioSink(audioSink); + try { + audioSink.connectIfNeeded(); + } catch (IOException e) { + logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})", + getHost(), e.getMessage()); + } @SuppressWarnings("unchecked") ServiceRegistration reg = (ServiceRegistration) bundleContext .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>()); @@ -124,12 +131,17 @@ public void dispose() { refreshJob = null; } updateStatus(ThingStatus.OFFLINE); + bridgeHandler.unregisterDeviceStatusListener(this); bridgeHandler = null; logger.trace("Thing {} {} disposed.", getThing().getUID(), name); super.dispose(); + if (audioSink != null) { + audioSink.disconnect(); + } + // Unregister the potential pulse audio sink's audio sink - ServiceRegistration reg = audioSinkRegistrations.get(getThing().getUID().toString()); + ServiceRegistration reg = audioSinkRegistrations.remove(getThing().getUID().toString()); if (reg != null) { logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID()); reg.unregister(); @@ -325,9 +337,20 @@ public String getHost() { } } + /** + * This method will scan the pulseaudio server to find the port on which the module/sink is listening + * If no module is listening, then it will command the module to load on the pulse audio server, + * + * @return + */ public int getSimpleTcpPort() { - return ((BigDecimal) getThing().getConfiguration() + Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration() .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue(); + + PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler(); + AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name); + return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref) + .orElse(simpleTcpPortPref); } @Override diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml index 2e014afdf2058..9f98aca6ace7f 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml @@ -10,6 +10,7 @@ represents a pulseaudio sink + Speaker @@ -22,13 +23,14 @@ The name of one specific device. - - Activation of a corresponding sink in openHAB (needs module-simple-protocol-tcp loaded on the server) + + Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the + pulseaudio server) false - Port of the module-simple-protocol-tcp listening for this sink + Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server 4711 From 80af598db548c171fef519254807595b58e44e1e Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Mon, 5 Apr 2021 18:29:26 +0200 Subject: [PATCH 4/5] Small corrections after reviews initialize audiosink in a thread with scheduler.submit clear some warning related code. Signed-off-by: Gwendal Roulleau Better interruptexception handling --- .../internal/PulseAudioAudioSink.java | 37 +++++++++---------- .../pulseaudio/internal/PulseaudioClient.java | 9 ++--- .../internal/handler/PulseaudioHandler.java | 28 ++++++++++---- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java index 93ae3baa9246c..d1fe0705848e2 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.Socket; -import java.net.UnknownHostException; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -104,10 +103,10 @@ public String getId() { /** * Connect to pulseaudio with the simple protocol * - * @throws UnknownHostException * @throws IOException + * @throws InterruptedException when interrupted during the loading module wait */ - public void connectIfNeeded() throws IOException { + public void connectIfNeeded() throws IOException, InterruptedException { Socket clientSocketLocal = clientSocket; if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { String host = pulseaudioHandler.getHost(); @@ -149,30 +148,28 @@ public void process(@Nullable AudioStream audioStream) audioStream.getFormat()); } - try { - connectIfNeeded(); - if (audioInputStream != null && clientSocket != null) { - // send raw audio to the socket and to pulse audio - audioInputStream.transferTo(clientSocket.getOutputStream()); - } - } catch (IOException e) { // second try, as the socket disconnection is sometimes not well detected - disconnect(); + for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed try { connectIfNeeded(); if (audioInputStream != null && clientSocket != null) { // send raw audio to the socket and to pulse audio audioInputStream.transferTo(clientSocket.getOutputStream()); + break; } - } catch (IOException e1) { - if (clientSocket != null) { - logger.warn( - "Error while trying to send audio to pulseaudio audio sink. Cannot connect to port {} (ip {}), error: {}", - clientSocket.getPort(), pulseaudioHandler.getHost(), e1.getMessage()); - } else { - logger.warn( - "Error while trying to send audio to pulseaudio audio sink. Cannot connect. (error: {})", - e1.getMessage()); + } catch (IOException e) { + disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown + if (countAttempt == 2) { // we won't retry : log and quit + if (logger.isWarnEnabled()) { + String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown"; + logger.warn( + "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}", + pulseaudioHandler.getHost(), port, e.getMessage()); + } + break; } + } catch (InterruptedException ie) { + logger.info("Interrupted during sink audio connection: {}", ie.getMessage()); + break; } } } finally { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index d5249b6aa0172..7638aae4b63f4 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -388,9 +388,10 @@ public void setVolume(AbstractAudioDeviceConfig item, int vol) { * @param item the sink we are searching for * @param simpleTcpPortPref the port to use if we have to load the module * @return the port on which the module is listening + * @throws InterruptedException */ public Optional loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item, - Integer simpleTcpPortPref) { + Integer simpleTcpPortPref) throws InterruptedException { int currentTry = 0; int simpleTcpPortToTry = simpleTcpPortPref; do { @@ -403,11 +404,7 @@ public Optional loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDevice + simpleTcpPortToTry); simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024 } - try { // let time for the module to load - Thread.sleep(100); - } catch (InterruptedException e) { - } - + Thread.sleep(100); currentTry++; } while (currentTry < 3); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index 3da3ca8e7f65e..88690998ac819 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -106,22 +106,35 @@ public void initialize() { Boolean sinkActivated = (Boolean) thing.getConfiguration() .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION); if (sinkActivated != null && sinkActivated) { + audioSinkSetup(); + } + } + } + + private void audioSinkSetup() { + final PulseaudioHandler thisHandler = this; + scheduler.submit(new Runnable() { + @Override + public void run() { // Register the sink as an audio sink in openhab logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID()); - PulseAudioAudioSink audioSink = new PulseAudioAudioSink(this); + PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler); setAudioSink(audioSink); try { audioSink.connectIfNeeded(); } catch (IOException e) { logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})", getHost(), e.getMessage()); + } catch (InterruptedException i) { + logger.info("Interrupted during sink audio connection: {}", i.getMessage()); + return; } @SuppressWarnings("unchecked") ServiceRegistration reg = (ServiceRegistration) bundleContext .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>()); audioSinkRegistrations.put(thing.getUID().toString(), reg); } - } + }); } @Override @@ -327,10 +340,10 @@ public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig devi } } - @SuppressWarnings("null") public String getHost() { - if (getBridge() != null) { - return (String) getBridge().getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); + Bridge bridge = getBridge(); + if (bridge != null) { + return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST); } else { logger.error("A bridge must be configured for this pulseaudio thing"); return "null"; @@ -341,9 +354,10 @@ public String getHost() { * This method will scan the pulseaudio server to find the port on which the module/sink is listening * If no module is listening, then it will command the module to load on the pulse audio server, * - * @return + * @return the port on which the pulseaudio server is listening for this sink + * @throws InterruptedException when interrupted during the loading module wait */ - public int getSimpleTcpPort() { + public int getSimpleTcpPort() throws InterruptedException { Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration() .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue(); From 9e269b852ede3d9a6e270c4544e1067de7522978 Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Mon, 5 Apr 2021 19:25:07 +0200 Subject: [PATCH 5/5] Fix two small concurrency bugs Signed-off-by: Gwendal Roulleau --- .../pulseaudio/internal/PulseaudioClient.java | 20 ++++++++++--------- .../handler/PulseaudioBridgeHandler.java | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 7638aae4b63f4..54cbc552b2a21 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -141,28 +141,30 @@ public boolean isConnected() { /** * updates the item states and their relationships */ - public void update() { - modules.clear(); - modules.addAll(Parser.parseModules(listModules())); + public synchronized void update() { + // one step copy + modules = new ArrayList(Parser.parseModules(listModules())); - items.clear(); + List newItems = new ArrayList<>(); // prepare new list before assigning it + newItems.clear(); if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sinks"); - items.addAll(Parser.parseSinks(listSinks(), this)); + newItems.addAll(Parser.parseSinks(listSinks(), this)); } if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sources"); - items.addAll(Parser.parseSources(listSources(), this)); + newItems.addAll(Parser.parseSources(listSources(), this)); } if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading sink-inputs"); - items.addAll(Parser.parseSinkInputs(listSinkInputs(), this)); + newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this)); } if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) { logger.debug("reading source-outputs"); - items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this)); + newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this)); } - logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), items.size()); + logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size()); + items = newItems; } private String listModules() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java index ec4f104c18bf4..c91ecfe2c55de 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java @@ -158,7 +158,9 @@ public void dispose() { if (pollingJob != null) { pollingJob.cancel(true); } - client.disconnect(); + if (client != null) { + client.disconnect(); + } super.dispose(); }