From 094cb7f12d110ccc0b1de3c870f71a8523a21060 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Thu, 3 Mar 2022 13:01:07 +0100 Subject: [PATCH] [pulseaudio] register audio sources in openhab (#12376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pulseaudio] register audio sources in openha * [pulseaudio] fix audio source reconnection * [pulseaudio] audio source check record property and customize SOTimeout * [pulseaudio] use pipe streams * [pulseaudio] synchronize commands and update after module load Signed-off-by: Miguel Álvarez Díez --- .../org.openhab.binding.pulseaudio/README.md | 12 +- .../internal/PulseAudioAudioSink.java | 99 +------ .../internal/PulseAudioAudioSource.java | 249 ++++++++++++++++++ .../PulseAudioBindingConfiguration.java | 2 +- .../internal/PulseaudioBindingConstants.java | 8 + .../pulseaudio/internal/PulseaudioClient.java | 109 +++++--- .../PulseaudioSimpleProtocolStream.java | 119 +++++++++ .../handler/DeviceStatusListener.java | 2 + .../internal/handler/PulseaudioHandler.java | 165 ++++++++++-- .../main/resources/OH-INF/binding/binding.xml | 2 +- .../OH-INF/i18n/pulseaudio.properties | 23 ++ .../src/main/resources/OH-INF/thing/sink.xml | 7 + .../main/resources/OH-INF/thing/source.xml | 52 ++++ 13 files changed, 696 insertions(+), 153 deletions(-) create mode 100644 bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSource.java create mode 100644 bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java diff --git a/bundles/org.openhab.binding.pulseaudio/README.md b/bundles/org.openhab.binding.pulseaudio/README.md index cf41c9ba7..82abe6815 100644 --- a/bundles/org.openhab.binding.pulseaudio/README.md +++ b/bundles/org.openhab.binding.pulseaudio/README.md @@ -20,7 +20,7 @@ The Pulseaudio bridge is discovered through mDNS in the local network. ## Binding Configuration (optional) -The Pulseaudio binding can be customized to handle different devices. The Sink support is activated by default and you need no further action to use it. If you want to use another type of device, or disable the Sink type, you have to switch the corresponding binding property. +The Pulseaudio binding can be customized to handle different devices. The Sink and Source support is activated by default and you need no further action to use it. If you want to use another type of device, or disable the Sink/Source type, you have to switch the corresponding binding property. - **sink:** Allow the binding to parse sink devices from the pulseaudio server - **source:** Allow the binding to parse source devices from the pulseaudio server @@ -31,7 +31,7 @@ You can use the GUI on the bindings page (click on the pulseaudio binding then " ``` binding.pulseaudio:sink=true -binding.pulseaudio:source=false +binding.pulseaudio:source=true binding.pulseaudio:sinkInput=false binding.pulseaudio:sourceOutput=false ``` @@ -59,6 +59,14 @@ Sink things can register themselves as audio sink in openHAB. MP3 and WAV files 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. + +## Audio source + +Source things can register themselves as audio source in openHAB. +WAV input format, rate and channels can be configured on the thing configuration. (defaults to pcm_signed,16000,1) +Use the appropriate parameter in the source thing to activate this possibility (activateSimpleProtocolSource). +This requires the module **module-simple-protocol-tcp** to be present on the target pulseaudio server. The binding will load this module on the pulseaudio server. + ## Full Example ### pulseaudio.things 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 58be3a598..7862a6d29 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 @@ -17,11 +17,8 @@ import java.net.Socket; import java.time.Duration; import java.time.Instant; import java.util.HashSet; -import java.util.Locale; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import javax.sound.sampled.UnsupportedAudioFileException; @@ -34,7 +31,6 @@ 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; @@ -42,25 +38,18 @@ import org.slf4j.LoggerFactory; * The audio sink for openhab, implemented by a connection to a pulseaudio sink * * @author Gwendal Roulleau - Initial contribution + * @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend + * from it. * */ @NonNullByDefault -public class PulseAudioAudioSink implements AudioSink { +public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream 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 ScheduledExecutorService scheduler; - - private @Nullable Socket clientSocket; - - private boolean isIdle = true; - - private @Nullable ScheduledFuture scheduledDisconnection; - static { SUPPORTED_FORMATS.add(AudioFormat.WAV); SUPPORTED_FORMATS.add(AudioFormat.MP3); @@ -68,60 +57,15 @@ public class PulseAudioAudioSink implements AudioSink { } public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) { - this.pulseaudioHandler = pulseaudioHandler; - this.scheduler = scheduler; - } - - @Override - public String getId() { - return pulseaudioHandler.getThing().getUID().toString(); - } - - @Override - public @Nullable String getLabel(@Nullable Locale locale) { - return pulseaudioHandler.getThing().getLabel(); - } - - /** - * Connect to pulseaudio with the simple protocol - * - * @throws IOException - * @throws InterruptedException when interrupted during the loading module wait - */ - public void connectIfNeeded() throws IOException, InterruptedException { - Socket clientSocketLocal = clientSocket; - if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.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() { - final Socket clientSocketLocal = clientSocket; - if (clientSocketLocal != null && isIdle) { - logger.debug("Disconnecting"); - try { - clientSocketLocal.close(); - } catch (IOException e) { - } - } else { - logger.debug("Stream still running or socket not open"); - } + super(pulseaudioHandler, scheduler); } @Override public void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { - if (audioStream == null) { return; } - try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) { for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed try { @@ -129,7 +73,7 @@ public class PulseAudioAudioSink implements AudioSink { final Socket clientSocketLocal = clientSocket; if (clientSocketLocal != null) { // send raw audio to the socket and to pulse audio - isIdle = false; + setIdle(false); Instant start = Instant.now(); normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream()); if (normalizedPCMStream.getDuration() != -1) { // ensure, if the sound has a duration @@ -147,12 +91,10 @@ public class PulseAudioAudioSink implements AudioSink { } 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()); - } + 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) { @@ -166,18 +108,7 @@ public class PulseAudioAudioSink implements AudioSink { } finally { scheduleDisconnect(); } - isIdle = true; - } - - public void scheduleDisconnect() { - 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); - } + setIdle(true); } @Override @@ -189,14 +120,4 @@ public class PulseAudioAudioSink implements AudioSink { 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/PulseAudioAudioSource.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSource.java new file mode 100644 index 000000000..9a1b39f1e --- /dev/null +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSource.java @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2010-2022 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.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.Socket; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; + +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.AudioException; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioSource; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.common.ThreadPoolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The audio source for openhab, implemented by a connection to a pulseaudio source using Simple TCP protocol + * + * @author Miguel Álvarez - Initial contribution + * + */ +@NonNullByDefault +public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implements AudioSource { + + private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class); + private final Set pipeOutputs = new HashSet<>(); + private final ScheduledExecutorService executor; + + private @Nullable Future pipeWriteTask; + + public PulseAudioAudioSource(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) { + super(pulseaudioHandler, scheduler); + executor = ThreadPoolManager + .getScheduledPool("OH-binding-" + pulseaudioHandler.getThing().getUID() + "-source"); + } + + @Override + public Set getSupportedFormats() { + var supportedFormats = new HashSet(); + var audioFormat = pulseaudioHandler.getSourceAudioFormat(); + if (audioFormat != null) { + supportedFormats.add(audioFormat); + } + return supportedFormats; + } + + @Override + public AudioStream getInputStream(AudioFormat audioFormat) throws AudioException { + try { + for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed + try { + connectIfNeeded(); + final Socket clientSocketLocal = clientSocket; + if (clientSocketLocal == null) { + break; + } + var sourceFormat = pulseaudioHandler.getSourceAudioFormat(); + if (sourceFormat == null) { + throw new AudioException("Unable to get source audio format"); + } + if (!audioFormat.isCompatible(sourceFormat)) { + throw new AudioException("Incompatible audio format requested"); + } + setIdle(true); + var pipeOutput = new PipedOutputStream(); + registerPipe(pipeOutput); + var pipeInput = new PipedInputStream(pipeOutput, 1024 * 20) { + @Override + public void close() throws IOException { + unregisterPipe(pipeOutput); + super.close(); + } + }; + // get raw audio from the pulse audio socket + return new PulseAudioStream(sourceFormat, pipeInput, (idle) -> { + setIdle(idle); + if (idle) { + scheduleDisconnect(); + } else { + // ensure pipe is writing + startPipeWrite(); + } + }); + } 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 + String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown"; + logger.warn( + "Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}", + pulseaudioHandler.getHost(), port, e.getMessage()); + setIdle(true); + throw e; + } + } catch (InterruptedException ie) { + logger.info("Interrupted during source audio connection: {}", ie.getMessage()); + setIdle(true); + throw new AudioException(ie); + } + countAttempt++; + } + } catch (IOException e) { + throw new AudioException(e); + } finally { + scheduleDisconnect(); + } + setIdle(true); + throw new AudioException("Unable to create input stream"); + } + + private synchronized void registerPipe(PipedOutputStream pipeOutput) { + this.pipeOutputs.add(pipeOutput); + startPipeWrite(); + } + + private void startPipeWrite() { + if (pipeWriteTask == null) { + this.pipeWriteTask = executor.submit(() -> { + int lengthRead; + byte[] buffer = new byte[1024]; + while (true) { + var stream = getSourceInputStream(); + if (stream != null) { + try { + lengthRead = stream.read(buffer); + for (var output : pipeOutputs) { + output.write(buffer, 0, lengthRead); + output.flush(); + } + } catch (IOException e) { + logger.warn("IOException while reading from pulse source: {}", e.getMessage()); + } catch (RuntimeException e) { + logger.warn("RuntimeException while reading from pulse source: {}", e.getMessage()); + } + } else { + logger.warn("Unable to get source input stream"); + } + } + }); + } + } + + private synchronized void unregisterPipe(PipedOutputStream pipeOutput) { + this.pipeOutputs.remove(pipeOutput); + stopPipeWriteTask(); + try { + pipeOutput.close(); + } catch (IOException ignored) { + } + } + + private void stopPipeWriteTask() { + var pipeWriteTask = this.pipeWriteTask; + if (pipeOutputs.isEmpty() && pipeWriteTask != null) { + pipeWriteTask.cancel(true); + this.pipeWriteTask = null; + } + } + + private @Nullable InputStream getSourceInputStream() { + try { + connectIfNeeded(); + } catch (IOException | InterruptedException ignored) { + } + try { + return (clientSocket != null) ? clientSocket.getInputStream() : null; + } catch (IOException ignored) { + return null; + } + } + + @Override + public void disconnect() { + stopPipeWriteTask(); + super.disconnect(); + } + + static class PulseAudioStream extends AudioStream { + private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class); + private final AudioFormat format; + private final InputStream input; + private final Consumer setIdle; + private boolean closed = false; + + public PulseAudioStream(AudioFormat format, InputStream input, Consumer setIdle) { + this.input = input; + this.format = format; + this.setIdle = setIdle; + } + + @Override + public AudioFormat getFormat() { + return format; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int bytesRead = read(b); + if (-1 == bytesRead) { + return bytesRead; + } + Byte bb = Byte.valueOf(b[0]); + return bb.intValue(); + } + + @Override + public int read(byte @Nullable [] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte @Nullable [] b, int off, int len) throws IOException { + logger.trace("reading from pulseaudio stream"); + if (closed) { + throw new IOException("Stream is closed"); + } + setIdle.accept(false); + return input.read(b, off, len); + } + + @Override + public void close() throws IOException { + closed = true; + setIdle.accept(true); + input.close(); + } + }; +} diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java index 6c16dbe4f..7de2c44a7 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java @@ -28,7 +28,7 @@ public class PulseAudioBindingConfiguration { public boolean sink = true; - public boolean source = false; + public boolean source = true; public boolean sinkInput = false; 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 c9fc05a18..a8277e88b 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 @@ -20,6 +20,7 @@ import org.openhab.core.thing.ThingTypeUID; * used across the whole binding. * * @author Tobias Bräutigam - Initial contribution + * @author Miguel Álvarez - Add new configuration options to sink and source */ @NonNullByDefault public class PulseaudioBindingConstants { @@ -51,6 +52,13 @@ 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 DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT = "simpleProtocolSinkIdleTimeout"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION = "activateSimpleProtocolSource"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_PORT = "simpleProtocolSourcePort"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT = "simpleProtocolSourceIdleTimeout"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_RATE = "simpleProtocolSourceRate"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT = "simpleProtocolSourceFormat"; + public static final String DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS = "simpleProtocolSourceChannels"; + public static final String DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT = "simpleProtocolSOTimeout"; public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp"; public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711; 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 0204c6b95..94bf0d28e 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 @@ -17,6 +17,7 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.math.BigDecimal; import java.net.NoRouteToHostException; import java.net.Socket; import java.net.SocketException; @@ -28,6 +29,8 @@ import java.util.Optional; import java.util.Random; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; 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; @@ -47,14 +50,16 @@ import org.slf4j.LoggerFactory; * On the pulseaudio server the module-cli-protocol-tcp has to be loaded. * * @author Tobias Bräutigam - Initial contribution + * @author Miguel Álvarez - changes for loading audio source module and nullability annotations */ +@NonNullByDefault public class PulseaudioClient { private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class); private String host; private int port; - private Socket client; + private @Nullable Socket client; private List items; private List modules; @@ -195,7 +200,7 @@ public class PulseaudioClient { * @param id * @return the corresponding {@link Module} to the given id */ - public Module getModule(int id) { + public @Nullable Module getModule(int id) { for (Module module : modules) { if (module.getId() == id) { return module; @@ -220,7 +225,7 @@ public class PulseaudioClient { * * @return the corresponding {@link Sink} to the given name */ - public Sink getSink(String name) { + public @Nullable Sink getSink(String name) { for (AbstractAudioDeviceConfig item : items) { if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink) { return (Sink) item; @@ -234,7 +239,7 @@ public class PulseaudioClient { * * @return the corresponding {@link Sink} to the given id */ - public Sink getSink(int id) { + public @Nullable Sink getSink(int id) { for (AbstractAudioDeviceConfig item : items) { if (item.getId() == id && item instanceof Sink) { return (Sink) item; @@ -248,7 +253,7 @@ public class PulseaudioClient { * * @return the corresponding {@link SinkInput} to the given name */ - public SinkInput getSinkInput(String name) { + public @Nullable SinkInput getSinkInput(String name) { for (AbstractAudioDeviceConfig item : items) { if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) { return (SinkInput) item; @@ -262,7 +267,7 @@ public class PulseaudioClient { * * @return the corresponding {@link SinkInput} to the given id */ - public SinkInput getSinkInput(int id) { + public @Nullable SinkInput getSinkInput(int id) { for (AbstractAudioDeviceConfig item : items) { if (item.getId() == id && item instanceof SinkInput) { return (SinkInput) item; @@ -276,7 +281,7 @@ public class PulseaudioClient { * * @return the corresponding {@link Source} to the given name */ - public Source getSource(String name) { + public @Nullable Source getSource(String name) { for (AbstractAudioDeviceConfig item : items) { if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) { return (Source) item; @@ -290,7 +295,7 @@ public class PulseaudioClient { * * @return the corresponding {@link Source} to the given id */ - public Source getSource(int id) { + public @Nullable Source getSource(int id) { for (AbstractAudioDeviceConfig item : items) { if (item.getId() == id && item instanceof Source) { return (Source) item; @@ -304,7 +309,7 @@ public class PulseaudioClient { * * @return the corresponding {@link SourceOutput} to the given name */ - public SourceOutput getSourceOutput(String name) { + public @Nullable SourceOutput getSourceOutput(String name) { for (AbstractAudioDeviceConfig item : items) { if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) { return (SourceOutput) item; @@ -318,7 +323,7 @@ public class PulseaudioClient { * * @return the corresponding {@link SourceOutput} to the given id */ - public SourceOutput getSourceOutput(int id) { + public @Nullable SourceOutput getSourceOutput(int id) { for (AbstractAudioDeviceConfig item : items) { if (item.getId() == id && item instanceof SourceOutput) { return (SourceOutput) item; @@ -332,7 +337,7 @@ public class PulseaudioClient { * * @return the corresponding {@link AbstractAudioDeviceConfig} to the given name */ - public AbstractAudioDeviceConfig getGenericAudioItem(String name) { + public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) { for (AbstractAudioDeviceConfig item : items) { if (item.getPaName().equalsIgnoreCase(name)) { return item; @@ -351,7 +356,7 @@ public class PulseaudioClient { * @param item the {@link Sink} to handle * @param mute mutes the sink if true, unmutes if false */ - public void setMute(AbstractAudioDeviceConfig item, boolean mute) { + public void setMute(@Nullable AbstractAudioDeviceConfig item, boolean mute) { if (item == null) { return; } @@ -395,20 +400,27 @@ public class PulseaudioClient { * @throws InterruptedException */ public Optional loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item, - Integer simpleTcpPortPref) throws InterruptedException { + Integer simpleTcpPortPref, @Nullable String format, @Nullable BigDecimal rate, + @Nullable BigDecimal channels) throws InterruptedException { int currentTry = 0; int simpleTcpPortToTry = simpleTcpPortPref; + String itemType = getItemCommandName(item); do { - Optional simplePort = findSimpleProtocolTcpModule(item); + Optional simplePort = findSimpleProtocolTcpModule(item, format, rate, channels); if (simplePort.isPresent()) { return simplePort; } else { - sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port=" - + simpleTcpPortToTry); + String moduleOptions = itemType + "=" + item.getPaName() + " port=" + simpleTcpPortToTry; + if (item instanceof Source && format != null && rate != null && channels != null) { + moduleOptions = moduleOptions + String.format(" record=true format=%s rate=%d channels=%d", format, + rate.longValue(), channels.intValue()); + } + sendRawCommand("load-module module-simple-protocol-tcp " + moduleOptions); simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024 } Thread.sleep(100); + update(); currentTry++; } while (currentTry < 3); @@ -424,14 +436,49 @@ public class PulseaudioClient { * @param item * @return */ - private Optional findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) { - update(); - + private Optional findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item, @Nullable String format, + @Nullable BigDecimal rate, @Nullable BigDecimal channels) { + String itemType = getItemCommandName(item); + if (itemType == null) { + return Optional.empty(); + } List modulesCopy = new ArrayList(modules); + var isSource = item instanceof Source; 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 + .filter(module -> { + boolean nameMatch = extractArgumentFromLine(itemType, module.getArgument()) // extract sick|source + .map(name -> name.equals(item.getPaName())).orElse(false); + if (isSource && nameMatch) { + boolean recordStream = extractArgumentFromLine("record", module.getArgument()) + .map("true"::equals).orElse(false); + if (!recordStream) { + return false; + } + if (format != null) { + boolean rateMatch = extractArgumentFromLine("format", module.getArgument()) + .map(format::equals).orElse(false); + if (!rateMatch) { + return false; + } + } + if (rate != null) { + boolean rateMatch = extractArgumentFromLine("rate", module.getArgument()) + .map(value -> Long.parseLong(value) == rate.longValue()).orElse(false); + if (!rateMatch) { + return false; + } + } + if (channels != null) { + boolean channelsMatch = extractArgumentFromLine("channels", module.getArgument()) + .map(value -> Integer.parseInt(value) == channels.intValue()).orElse(false); + if (!channelsMatch) { + return false; + } + } + } + return nameMatch; + }) // 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 @@ -458,7 +505,7 @@ public class PulseaudioClient { * @param item * @return */ - private String getItemCommandName(AbstractAudioDeviceConfig item) { + private @Nullable String getItemCommandName(AbstractAudioDeviceConfig item) { if (item instanceof Sink) { return ITEM_SINK; } else if (item instanceof Source) { @@ -478,7 +525,7 @@ public class PulseaudioClient { * @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible * values from 0 - 100) */ - public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) { + public void setVolumePercent(@Nullable AbstractAudioDeviceConfig item, int vol) { int volumeToSet = vol; if (item == null) { return; @@ -505,7 +552,7 @@ public class PulseaudioClient { * @param combinedSink the combined sink which slaves should be changed * @param sinks the list of new slaves */ - public void setCombinedSinkSlaves(Sink combinedSink, List sinks) { + public void setCombinedSinkSlaves(@Nullable Sink combinedSink, List sinks) { if (combinedSink == null || !combinedSink.isCombinedSink()) { return; } @@ -528,7 +575,7 @@ public class PulseaudioClient { * @param sinkInput the sink-input to be rerouted * @param sink the new sink the sink-input should be routed to */ - public void moveSinkInput(SinkInput sinkInput, Sink sink) { + public void moveSinkInput(@Nullable SinkInput sinkInput, @Nullable Sink sink) { if (sinkInput == null || sink == null) { return; } @@ -542,7 +589,7 @@ public class PulseaudioClient { * @param sourceOutput the source-output to be rerouted * @param source the new source the source-output should be routed to */ - public void moveSourceOutput(SourceOutput sourceOutput, Source source) { + public void moveSourceOutput(@Nullable SourceOutput sourceOutput, @Nullable Source source) { if (sourceOutput == null || source == null) { return; } @@ -556,7 +603,7 @@ public class PulseaudioClient { * @param source the source which state should be changed * @param suspend suspend it or not */ - public void suspendSource(Source source, boolean suspend) { + public void suspendSource(@Nullable Source source, boolean suspend) { if (source == null) { return; } @@ -577,7 +624,7 @@ public class PulseaudioClient { * @param sink the sink which state should be changed * @param suspend suspend it or not */ - public void suspendSink(Sink sink, boolean suspend) { + public void suspendSink(@Nullable Sink sink, boolean suspend) { if (sink == null) { return; } @@ -613,9 +660,9 @@ public class PulseaudioClient { update(); } - private void sendRawCommand(String command) { + private synchronized void sendRawCommand(String command) { checkConnection(); - if (client != null) { + if (client != null && client.isConnected()) { try { PrintStream out = new PrintStream(client.getOutputStream(), true); logger.trace("sending command {} to pa-server {}", command, host); @@ -632,7 +679,7 @@ public class PulseaudioClient { logger.trace("_sendRawRequest({})", command); checkConnection(); String result = ""; - if (client != null) { + if (client != null && client.isConnected()) { try { PrintStream out = new PrintStream(client.getOutputStream(), true); out.print(command + "\r\n"); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java new file mode 100644 index 000000000..276ec38f7 --- /dev/null +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2022 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.net.Socket; +import java.util.Locale; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; +import org.openhab.core.library.types.PercentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A connection to a pulseaudio Simple TCP Protocol + * + * @author Gwendal Roulleau - Initial contribution + * @author Miguel Álvarez - Refactor some code from PulseAudioAudioSink here + * + */ +@NonNullByDefault +public abstract class PulseaudioSimpleProtocolStream { + + private final Logger logger = LoggerFactory.getLogger(PulseaudioSimpleProtocolStream.class); + + protected PulseaudioHandler pulseaudioHandler; + protected ScheduledExecutorService scheduler; + + protected @Nullable Socket clientSocket; + + private boolean isIdle = true; + + private @Nullable ScheduledFuture scheduledDisconnection; + + public PulseaudioSimpleProtocolStream(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) { + this.pulseaudioHandler = pulseaudioHandler; + this.scheduler = scheduler; + } + + /** + * Connect to pulseaudio with the simple protocol + * + * @throws IOException + * @throws InterruptedException when interrupted during the loading module wait + */ + public void connectIfNeeded() throws IOException, InterruptedException { + Socket clientSocketLocal = clientSocket; + if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { + logger.debug("Simple TCP Stream connecting"); + String host = pulseaudioHandler.getHost(); + int port = pulseaudioHandler.getSimpleTcpPort(); + clientSocket = new Socket(host, port); + clientSocket.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout()); + } + } + + /** + * Disconnect the socket to pulseaudio simple protocol + */ + public void disconnect() { + final Socket clientSocketLocal = clientSocket; + if (clientSocketLocal != null && isIdle) { + logger.debug("Simple TCP Stream disconnecting"); + try { + clientSocketLocal.close(); + } catch (IOException ignored) { + } + } else { + logger.debug("Stream still running or socket not open"); + } + } + + public void scheduleDisconnect() { + 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); + } + } + + public PercentType getVolume() { + return new PercentType(pulseaudioHandler.getLastVolume()); + } + + public void setVolume(PercentType volume) { + pulseaudioHandler.setVolume(volume.intValue()); + } + + public String getId() { + return pulseaudioHandler.getThing().getUID().toString(); + } + + public String getLabel(@Nullable Locale locale) { + var label = pulseaudioHandler.getThing().getLabel(); + return label != null ? label : pulseaudioHandler.getThing().getUID().getId(); + } + + public void setIdle(boolean idle) { + isIdle = idle; + } +} diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java index 30f1fac3a..4df412417 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.pulseaudio.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingUID; @@ -23,6 +24,7 @@ import org.openhab.core.thing.ThingUID; * @author Tobias Bräutigam - Initial contribution * */ +@NonNullByDefault public interface DeviceStatusListener { /** 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 c145dcaae..df107b91b 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 @@ -28,12 +28,18 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink; +import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSource; 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.binding.pulseaudio.internal.items.Source; +import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioSink; +import org.openhab.core.audio.AudioSource; import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; @@ -62,29 +68,28 @@ import org.slf4j.LoggerFactory; * sent to one of the channels. * * @author Tobias Bräutigam - Initial contribution + * @author Miguel Álvarez - Register audio source and refactor */ +@NonNullByDefault public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener { public static final Set SUPPORTED_THING_TYPES_UIDS = Collections .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE, SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet())); - - private int refresh = 60; // refresh every minute as default - private ScheduledFuture refreshJob; - - private PulseaudioBridgeHandler bridgeHandler; - private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class); + private final int refresh = 60; // refresh every minute as default - private String name; + private @Nullable PulseaudioBridgeHandler bridgeHandler; + private @Nullable String name; + private @Nullable ScheduledFuture refreshJob; + private @Nullable PulseAudioAudioSink audioSink; + private @Nullable PulseAudioAudioSource audioSource; + private @Nullable Integer savedVolume; - private PulseAudioAudioSink audioSink; + private final Map> audioSinkRegistrations = new ConcurrentHashMap<>(); + private final Map> audioSourceRegistrations = new ConcurrentHashMap<>(); - private Integer savedVolume; - - private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); - - private BundleContext bundleContext; + private final BundleContext bundleContext; public PulseaudioHandler(Thing thing, BundleContext bundleContext) { super(thing); @@ -109,6 +114,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL audioSinkSetup(); } } + // if it's a SOURCE thing, then maybe we have to activate the audio source + if (SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) { + // check the property to see if we it's enabled : + Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION); + if (sourceActivated != null && sourceActivated) { + audioSourceSetup(); + } + } } private void audioSinkSetup() { @@ -139,6 +152,34 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL }); } + private void audioSourceSetup() { + final PulseaudioHandler thisHandler = this; + scheduler.submit(new Runnable() { + @Override + public void run() { + // Register the source as an audio source in openhab + logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID()); + PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler); + setAudioSource(audioSource); + try { + audioSource.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 source audio connection: {}", i.getMessage()); + return; + } finally { + audioSource.scheduleDisconnect(); + } + @SuppressWarnings("unchecked") + ServiceRegistration reg = (ServiceRegistration) bundleContext + .registerService(AudioSource.class.getName(), audioSource, new Hashtable<>()); + audioSourceRegistrations.put(thing.getUID().toString(), reg); + } + }); + } + @Override public void dispose() { if (refreshJob != null && !refreshJob.isCancelled()) { @@ -152,16 +193,23 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL } logger.trace("Thing {} {} disposed.", getThing().getUID(), name); super.dispose(); - if (audioSink != null) { audioSink.disconnect(); } - + if (audioSource != null) { + audioSource.disconnect(); + } // Unregister the potential pulse audio sink's audio sink - ServiceRegistration reg = audioSinkRegistrations.remove(getThing().getUID().toString()); - if (reg != null) { + ServiceRegistration sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString()); + if (sinkReg != null) { logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID()); - reg.unregister(); + sinkReg.unregister(); + } + // Unregister the potential pulse audio source's audio sources + ServiceRegistration sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString()); + if (sourceReg != null) { + logger.trace("Unregistering the audio sync service for pulse audio source thing {}", getThing().getUID()); + sourceReg.unregister(); } } @@ -172,7 +220,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL if (bridgeHandler != null) { if (bridgeHandler.getDevice(name) == null) { updateStatus(ThingStatus.OFFLINE); - bridgeHandler = null; + this.bridgeHandler = null; } else { updateStatus(ThingStatus.ONLINE); } @@ -182,14 +230,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL } } catch (Exception e) { logger.debug("Exception occurred during execution: {}", e.getMessage(), e); - bridgeHandler = null; + this.bridgeHandler = null; } }; refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS); } - private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() { + private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() { if (this.bridgeHandler == null) { Bridge bridge = getBridge(); if (bridge == null) { @@ -367,18 +415,73 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL * @throws InterruptedException when interrupted during the loading module wait */ public int getSimpleTcpPort() throws InterruptedException { - Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration() - .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue(); - - PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler(); + var bridgeHandler = getPulseaudioBridgeHandler(); AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name); - return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref) - .orElse(simpleTcpPortPref); + String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT + : DEVICE_PARAMETER_AUDIO_SINK_PORT; + BigDecimal simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration().get(simpleTcpPortPrefName)); + int simpleTcpPort = simpleTcpPortPref != null ? simpleTcpPortPref.intValue() + : MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT; + String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT)); + BigDecimal simpleRate = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE); + BigDecimal simpleChannels = (BigDecimal) getThing().getConfiguration() + .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS); + return getPulseaudioBridgeHandler().getClient() + .loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels) + .orElse(simpleTcpPort); + } + + public @Nullable AudioFormat getSourceAudioFormat() { + String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT)); + BigDecimal simpleRate = ((BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE)); + BigDecimal simpleChannels = ((BigDecimal) getThing().getConfiguration() + .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS)); + if (simpleFormat == null || simpleRate == null || simpleChannels == null) { + return null; + } + switch (simpleFormat) { + case "u8": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 8, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s16le": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s16be": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, true, 16, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s24le": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 24, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s24be": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 24, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s32le": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 32, 1, + simpleRate.longValue(), simpleChannels.intValue()); + case "s32be": + return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 32, 1, + simpleRate.longValue(), simpleChannels.intValue()); + default: + logger.warn("unsupported format {}", simpleFormat); + return null; + } } public int getIdleTimeout() { - return ((BigDecimal) getThing().getConfiguration() - .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT)).intValue(); + var handler = getPulseaudioBridgeHandler(); + if (handler == null) { + return 30000; + } + AbstractAudioDeviceConfig device = handler.getDevice(name); + String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT + : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT; + var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName); + return idleTimeout != null ? idleTimeout.intValue() : 30000; + } + + public int getBasicProtocolSOTimeout() { + var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT); + return soTimeout != null ? soTimeout.intValue() : 500; } @Override @@ -400,4 +503,8 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL public void setAudioSink(PulseAudioAudioSink audioSink) { this.audioSink = audioSink; } + + public void setAudioSource(PulseAudioAudioSource audioSource) { + this.audioSource = audioSource; + } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/binding/binding.xml index 15ea8c80a..e95a7f89a 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/binding/binding.xml @@ -20,7 +20,7 @@ Activate the import of source elements. - false + true diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties index 62471edca..2a56feb6c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties @@ -43,14 +43,37 @@ thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.label = Create an A thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.description = Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server) thing-type.config.pulseaudio.sink.name.label = Name thing-type.config.pulseaudio.sink.name.description = The name of one specific device. +thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout +thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently. thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.label = Idle Timeout thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.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. -1 for no disconnection. thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.label = Simple Protocol Port thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.description = Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server thing-type.config.pulseaudio.sinkInput.name.label = Name thing-type.config.pulseaudio.sinkInput.name.description = The name of one specific device. +thing-type.config.pulseaudio.source.activateSimpleProtocolSource.label = Create an Audio Source with simple-protocol-tcp +thing-type.config.pulseaudio.source.activateSimpleProtocolSource.description = Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server) thing-type.config.pulseaudio.source.name.label = Name thing-type.config.pulseaudio.source.name.description = The name of one specific device. +thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout +thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently. +thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.label = Simple Protocol Channels +thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.description = The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.label = Simple Protocol Format +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.description = The audio format to be used by module-simple-protocol-tcp on the pulseaudio server +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.u8 = PCM signed 8-bit +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16le = PCM signed 16-bit little-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16be = PCM signed 16-bit big-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24le = PCM unsigned 24-bit little-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24be = PCM unsigned 24-bit big-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32le = PCM signed 32-bit little-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32be = PCM signed 32-bit big-endian +thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.label = Idle Timeout +thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.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 source can go to idle mode. -1 for no disconnection. +thing-type.config.pulseaudio.source.simpleProtocolSourcePort.label = Simple Protocol Port +thing-type.config.pulseaudio.source.simpleProtocolSourcePort.description = Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server +thing-type.config.pulseaudio.source.simpleProtocolSourceRate.label = Simple Protocol Rate +thing-type.config.pulseaudio.source.simpleProtocolSourceRate.description = The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server thing-type.config.pulseaudio.sourceOutput.name.label = Name thing-type.config.pulseaudio.sourceOutput.name.description = The name of one specific device. 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 121faa7a5..6e4a44fe9 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 @@ -40,6 +40,13 @@ 30000 + + + Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune + this option if the socket disconnect frequently. + 500 + true + diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml index f783b3485..9383b62bc 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml @@ -21,6 +21,58 @@ The name of one specific device. + + + Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the + pulseaudio server) + false + + + + Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server + 4710 + + + + 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 source can go to idle mode. -1 for no disconnection. + + 30000 + + + + The audio format to be used by module-simple-protocol-tcp on the pulseaudio server + s16le + true + + + + + + + + + + + + + The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server + 16000 + true + + + + The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server + 1 + true + + + + Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune + this option if the socket disconnect frequently. + 500 + true +