/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.pulseaudio.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.pulseaudio.cli.Parser;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.Module;
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.binding.pulseaudio.internal.items.SourceOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The client connects to a pulseaudio server via TCP. It reads the current state of the
* pulseaudio server (available sinks, sources,...) and can send commands to the server.
* The syntax of the commands is the same as for the pactl command line tool provided by pulseaudio.
*
* On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
*
* @author Tobias Bräutigam
* @since 1.2.0
*/
public class PulseaudioClient {
private static final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
private String host;
private int port;
private Socket client;
private List<AbstractAudioDeviceConfig> items;
private List<Module> modules;
/**
* corresponding name to execute actions on sink items
*/
private static String ITEM_SINK = "sink";
/**
* corresponding name to execute actions on source items
*/
private static String ITEM_SOURCE = "source";
/**
* corresponding name to execute actions on sink-input items
*/
private static String ITEM_SINK_INPUT = "sink-input";
/**
* corresponding name to execute actions on source-output items
*/
private static String ITEM_SOURCE_OUTPUT = "source-output";
/**
* command to list the loaded modules
*/
private static String CMD_LIST_MODULES = "list-modules";
/**
* command to list the sinks
*/
private static String CMD_LIST_SINKS = "list-sinks";
/**
* command to list the sources
*/
private static String CMD_LIST_SOURCES = "list-sources";
/**
* command to list the sink-inputs
*/
private static String CMD_LIST_SINK_INPUTS = "list-sink-inputs";
/**
* command to list the source-outputs
*/
private static String CMD_LIST_SOURCE_OUTPUTS = "list-source-outputs";
/**
* command to load a module
*/
private static String CMD_LOAD_MODULE = "load-module";
/**
* command to unload a module
*/
private static String CMD_UNLOAD_MODULE = "unload-module";
/**
* name of the module-combine-sink
*/
private static String MODULE_COMBINE_SINK = "module-combine-sink";
public PulseaudioClient() throws IOException {
this("localhost", 4712);
}
public PulseaudioClient(String host, int port) throws IOException {
this.host = host;
this.port = port;
items = new ArrayList<AbstractAudioDeviceConfig>();
modules = new ArrayList<Module>();
connect();
update();
}
public boolean isConnected() {
return client.isConnected();
}
/**
* updates the item states and their relationships
*/
public void update() {
modules.clear();
modules.addAll(Parser.parseModules(listModules()));
items.clear();
items.addAll(Parser.parseSinks(listSinks(), this));
items.addAll(Parser.parseSources(listSources(), this));
items.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
logger.debug(modules.size() + " modules and " + items.size() + " items updated");
}
private String listModules() {
return this._sendRawRequest(CMD_LIST_MODULES);
}
private String listSinks() {
return this._sendRawRequest(CMD_LIST_SINKS);
}
private String listSources() {
return this._sendRawRequest(CMD_LIST_SOURCES);
}
private String listSinkInputs() {
return this._sendRawRequest(CMD_LIST_SINK_INPUTS);
}
private String listSourceOutputs() {
return this._sendRawRequest(CMD_LIST_SOURCE_OUTPUTS);
}
/**
* retrieves a module by its id
*
* @param id
* @return the corresponding {@link Module} to the given <code>id</code>
*/
public Module getModule(int id) {
for (Module module : modules) {
if (module.getId() == id) {
return module;
}
}
return null;
}
/**
* send the command directly to the pulseaudio server
* for a list of available commands please take a look at
* http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/CLI
*
* @param command
*/
public void sendCommand(String command) {
_sendRawCommand(command);
}
/**
* retrieves a {@link Sink} by its name
*
* @return the corresponding {@link Sink} to the given <code>name</code>
*/
public Sink getSink(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getName().equalsIgnoreCase(name) && item instanceof Sink) {
return (Sink) item;
}
}
return null;
}
/**
* retrieves a {@link Sink} by its id
*
* @return the corresponding {@link Sink} to the given <code>id</code>
*/
public Sink getSink(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Sink) {
return (Sink) item;
}
}
return null;
}
/**
* retrieves a {@link SinkInput} by its name
*
* @return the corresponding {@link SinkInput} to the given <code>name</code>
*/
public SinkInput getSinkInput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getName().equalsIgnoreCase(name) && item instanceof SinkInput) {
return (SinkInput) item;
}
}
return null;
}
/**
* retrieves a {@link SinkInput} by its id
*
* @return the corresponding {@link SinkInput} to the given <code>id</code>
*/
public SinkInput getSinkInput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SinkInput) {
return (SinkInput) item;
}
}
return null;
}
/**
* retrieves a {@link Source} by its name
*
* @return the corresponding {@link Source} to the given <code>name</code>
*/
public Source getSource(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getName().equalsIgnoreCase(name) && item instanceof Source) {
return (Source) item;
}
}
return null;
}
/**
* retrieves a {@link Source} by its id
*
* @return the corresponding {@link Source} to the given <code>id</code>
*/
public Source getSource(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Source) {
return (Source) item;
}
}
return null;
}
/**
* retrieves a {@link SourceOutput} by its name
*
* @return the corresponding {@link SourceOutput} to the given <code>name</code>
*/
public SourceOutput getSourceOutput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
return (SourceOutput) item;
}
}
return null;
}
/**
* retrieves a {@link SourceOutput} by its id
*
* @return the corresponding {@link SourceOutput} to the given <code>id</code>
*/
public SourceOutput getSourceOutput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SourceOutput) {
return (SourceOutput) item;
}
}
return null;
}
/**
* retrieves a {@link AbstractAudioDeviceConfig} by its name
*
* @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
*/
public AbstractAudioDeviceConfig getGenericAudioItem(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getName().equalsIgnoreCase(name)) {
return item;
}
}
return null;
}
/**
* changes the <code>mute</code> state of the corresponding {@link Sink}
*
* @param sink the {@link Sink} to handle
* @param mute mutes the sink if true, unmutes if false
*/
public void setMute(AbstractAudioDeviceConfig item, boolean mute) {
if (item == null) {
return;
}
String itemCommandName = getItemCommandName(item);
if (itemCommandName == null) {
return;
}
String muteString = mute ? "1" : "0";
_sendRawCommand("set-" + itemCommandName + "-mute " + item.getId() + " " + muteString);
// update internal data
item.setMuted(mute);
}
/**
* change the volume of a {@link AbstractAudioDeviceConfig}
*
* @param item the {@link AbstractAudioDeviceConfig} to handle
* @param vol the new volume value the {@link AbstractAudioDeviceConfig} should be changed to (possible values from
* 0 - 65536)
*/
public void setVolume(AbstractAudioDeviceConfig item, int vol) {
if (item == null) {
return;
}
String itemCommandName = getItemCommandName(item);
if (itemCommandName == null) {
return;
}
_sendRawCommand("set-" + itemCommandName + "-volume " + item.getId() + " " + vol);
item.setVolume(Math.round(100f / 65536f * vol));
}
/**
* returns the item names that can be used in commands
*
* @param item
* @return
*/
private String getItemCommandName(AbstractAudioDeviceConfig item) {
if (item instanceof Sink) {
return ITEM_SINK;
} else if (item instanceof Source) {
return ITEM_SOURCE;
} else if (item instanceof SinkInput) {
return ITEM_SINK_INPUT;
} else if (item instanceof SourceOutput) {
return ITEM_SOURCE_OUTPUT;
}
return null;
}
/**
* change the volume of a {@link AbstractAudioDeviceConfig}
*
* @param item the {@link AbstractAudioDeviceConfig} to handle
* @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) {
if (item == null) {
return;
}
if (vol <= 100) {
vol = toAbsoluteVolume(vol);
}
setVolume(item, vol);
}
/**
* transform a percent volume to a value that can be send to the pulseaudio server (0-65536)
*
* @param percent
* @return
*/
private int toAbsoluteVolume(int percent) {
return (int) Math.round(65536f / 100f * Double.valueOf(percent));
}
/**
* changes the combined sinks slaves to the given <code>sinks</code>
*
* @param combinedSink the combined sink which slaves should be changed
* @param sinks the list of new slaves
*/
public void setCombinedSinkSlaves(Sink combinedSink, List<Sink> sinks) {
if (combinedSink == null || !combinedSink.isCombinedSink()) {
return;
}
List<String> slaves = new ArrayList<String>();
for (Sink sink : sinks) {
slaves.add(sink.getName());
}
// 1. delete old combined-sink
_sendRawCommand(CMD_UNLOAD_MODULE + " " + combinedSink.getModule().getId());
// 2. add new combined-sink with same name and all slaves
_sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getName()
+ " slaves=" + StringUtils.join(slaves, ","));
// 3. update internal data structure because the combined sink has a new number + other slaves
update();
}
/**
* changes the combined sinks slaves to the given <code>sinks</code>
*
* @param combinedSink the combined sink which slaves should be changed
* @param sinks the list of new slaves
*/
public void setCombinedSinkSlaves(String combinedSinkName, List<Sink> sinks) {
if (getSink(combinedSinkName) != null) {
return;
}
List<String> slaves = new ArrayList<String>();
for (Sink sink : sinks) {
slaves.add(sink.getName());
}
// add new combined-sink with same name and all slaves
_sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSinkName + " slaves="
+ StringUtils.join(slaves, ","));
// update internal data structure because the combined sink is new
update();
}
private void _sendRawCommand(String command) {
checkConnection();
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
// System.out.println(command);
out.print(command + "\r\n");
out.close();
client.close();
} catch (IOException e) {
logger.error(e.getLocalizedMessage(), e);
}
}
private String _sendRawRequest(String command) {
checkConnection();
String result = "";
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
out.print(command + "\r\n");
InputStream instr = client.getInputStream();
try {
byte[] buff = new byte[1024];
int ret_read = 0;
int lc = 0;
do {
ret_read = instr.read(buff);
lc++;
if (ret_read > 0) {
String line = new String(buff, 0, ret_read);
// System.out.println("'"+line+"'");
if (line.endsWith(">>> ") && lc > 1) {
result += line.substring(0, line.length() - 4);
break;
}
result += line.trim();
}
} while (ret_read > 0);
} catch (SocketTimeoutException e) {
// Timeout -> send was has been received so far
return result;
} catch (IOException e) {
System.err.println("Exception while reading socket:" + e.getMessage());
}
instr.close();
out.close();
client.close();
return result;
} catch (IOException e) {
logger.error(e.getLocalizedMessage(), e);
}
return result;
}
private void checkConnection() {
if (client == null || client.isClosed() || !client.isConnected()) {
try {
connect();
} catch (IOException e) {
logger.error(e.getLocalizedMessage(), e);
}
}
}
/**
* Connects to the pulseaudio server (timeout 500ms)
*/
private void connect() throws IOException {
client = new Socket(host, port);
client.setSoTimeout(500);
}
/**
* Disconnects from the pulseaudio server
*/
public void disconnect() {
if (client != null) {
try {
client.close();
} catch (IOException e) {
logger.error(e.getLocalizedMessage(), e);
}
}
}
}