Skip to content

Commit

Permalink
Fix some registration errors and allow the binding to load the simple…
Browse files Browse the repository at this point in the history
… module remotely

Signed-off-by: Gwendal Roulleau <[email protected]>
  • Loading branch information
dalgwen committed Apr 3, 2021
1 parent 05471b4 commit 9fb5a19
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 16 deletions.
6 changes: 3 additions & 3 deletions bundles/org.openhab.binding.pulseaudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", 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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Boolean> TYPE_FILTERS = new HashMap<>();

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
Integer simpleTcpPortPref) {
int currentTry = 0;
int simpleTcpPortToTry = simpleTcpPortPref;
do {
Optional<Integer> 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<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) {
update();

List<Module> modulesCopy = new ArrayList<Module>(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
*
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
Expand All @@ -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<AudioSink> reg = audioSinkRegistrations.get(getThing().getUID().toString());
ServiceRegistration<AudioSink> 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();
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</supported-bridge-type-refs>
<label>A Pulseaudio Sink</label>
<description>represents a pulseaudio sink</description>
<category>Speaker</category>

<channels>
<channel id="volume" typeId="volume"/>
Expand All @@ -22,13 +23,14 @@
<description>The name of one specific device.</description>
</parameter>
<parameter name="activateSimpleProtocolSink" type="boolean" required="false">
<label>Sink by simple-protocol-tcp</label>
<description>Activation of a corresponding sink in openHAB (needs module-simple-protocol-tcp loaded on the server)</description>
<label>Create an Audio Sink with simple-protocol-tcp</label>
<description>Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the
pulseaudio server)</description>
<default>false</default>
</parameter>
<parameter name="simpleProtocolSinkPort" type="integer" required="false">
<label>Simple Protocol Port</label>
<description>Port of the module-simple-protocol-tcp listening for this sink</description>
<description>Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server</description>
<default>4711</default>
</parameter>
</config-description>
Expand Down

0 comments on commit 9fb5a19

Please sign in to comment.