[Onkyo] Set "CurrentURIMetaData" for "SetAVTransportURI" action (#17755) (#17770)

* [Onkyo] Set "CurrentURIMetaData" for "SetAVTransportURI" action (#17755)

Signed-off-by: Krzysztof Goworek <krzysztof.goworek@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
druciak 2024-11-21 17:20:02 +01:00 committed by Ciprian Pascu
parent e2ced84eb2
commit 40e0b845bd
4 changed files with 222 additions and 8 deletions

View File

@ -45,9 +45,9 @@ public class OnkyoAudioSink extends AudioSinkAsync {
private static final Set<AudioFormat> SUPPORTED_FORMATS = Set.of(AudioFormat.WAV, AudioFormat.MP3);
private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
private OnkyoHandler handler;
private AudioHTTPServer audioHTTPServer;
private @Nullable String callbackUrl;
private final OnkyoHandler handler;
private final AudioHTTPServer audioHTTPServer;
private final @Nullable String callbackUrl;
public OnkyoAudioSink(OnkyoHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
this.handler = handler;
@ -106,7 +106,7 @@ public class OnkyoAudioSink extends AudioSinkAsync {
tryClose(audioStream);
return;
}
handler.playMedia(url);
handler.playMedia(url, audioStream);
}
private void tryClose(@Nullable InputStream is) {

View File

@ -16,6 +16,7 @@ import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.onkyo.internal.OnkyoBindingConstants;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
import org.openhab.core.io.transport.upnp.UpnpIOService;
import org.openhab.core.library.types.StringType;
@ -35,7 +36,9 @@ public abstract class OnkyoUpnpHandler extends BaseThingHandler implements UpnpI
private final Logger logger = LoggerFactory.getLogger(OnkyoUpnpHandler.class);
private UpnpIOService service;
private final UpnpIOService service;
private final URIMetaDataProcessor uriMetaDataProcessor = new URIMetaDataProcessor();
public OnkyoUpnpHandler(Thing thing, UpnpIOService upnpIOService) {
super(thing);
@ -45,7 +48,7 @@ public abstract class OnkyoUpnpHandler extends BaseThingHandler implements UpnpI
protected void handlePlayUri(Command command) {
if (command instanceof StringType) {
try {
playMedia(command.toString());
playMedia(command.toString(), null);
} catch (IllegalStateException e) {
logger.warn("Cannot play URI ({})", e.getMessage());
@ -53,7 +56,7 @@ public abstract class OnkyoUpnpHandler extends BaseThingHandler implements UpnpI
}
}
public void playMedia(String url) {
public void playMedia(String url, AudioStream audioStream) {
stop();
removeAllTracksFromQueue();
@ -61,7 +64,7 @@ public abstract class OnkyoUpnpHandler extends BaseThingHandler implements UpnpI
url = "x-file-cifs:" + url;
}
setCurrentURI(url, "");
setCurrentURI(url, uriMetaDataProcessor.generate(url, audioStream));
play();
}

View File

@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2024 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.onkyo.internal.handler;
import static org.jupnp.model.XMLUtil.appendNewElement;
import java.io.StringWriter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.jupnp.transport.impl.PooledXmlProcessor;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Utility class for building metadata XML for UPnP "SetAVTransportURI" action.
*
* @author Krzysztof Goworek - Initial contribution
*/
public class URIMetaDataProcessor extends PooledXmlProcessor {
private static final String XML_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
private static final String DIDL_NAMESPACE_URI = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private static final String UPNP_NAMESPACE_URI = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Generates metadata XML for given audio stream.
*
* @param url media stream URL
* @param audioStream audio format specification
* @return generated XML document as {@code String}
*/
public String generate(String url, AudioStream audioStream) {
if (audioStream != null) {
try {
Document document = this.newDocument();
Element rootElement = document.createElementNS(DIDL_NAMESPACE_URI, "DIDL-Lite");
document.appendChild(rootElement);
rootElement.setAttributeNS(XML_NAMESPACE_URI, "xmlns:upnp", UPNP_NAMESPACE_URI);
Element itemElement = appendNewElement(document, rootElement, "item");
setAttributeIfNotNull(itemElement, "id", audioStream.getId());
appendNewElement(document, itemElement, "upnp:class", "object.item.audioItem", UPNP_NAMESPACE_URI);
Element resourceElement = appendNewElement(document, itemElement, "res", url);
setFormatAttributes(resourceElement, audioStream.getFormat());
return documentToString(document);
} catch (Exception e) {
logger.warn("Unable to build metadata for {}: {}", url, e.getMessage());
}
}
return "";
}
private void setFormatAttributes(Element resourceElement, AudioFormat format) {
setAttributeIfNotNull(resourceElement, "nrAudioChannels", format.getChannels());
setAttributeIfNotNull(resourceElement, "sampleFrequency", format.getFrequency());
setAttributeIfNotNull(resourceElement, "bitrate", format.getBitRate());
setAttributeIfNotNull(resourceElement, "protocolInfo", getProtocolInfo(format));
}
private String getProtocolInfo(AudioFormat format) {
String[] protocolInfo = { "http-get", "*", getFormatMimeType(format), "*" };
return String.join(":", protocolInfo);
}
private String getFormatMimeType(AudioFormat format) {
if (AudioFormat.MP3.isCompatible(format)) {
return "audio/mpeg";
} else if (AudioFormat.WAV.isCompatible(format)) {
return "audio/wav";
} else if (AudioFormat.OGG.isCompatible(format)) {
return "audio/ogg";
} else if (AudioFormat.AAC.isCompatible(format)) {
return "audio/aac";
} else if (AudioFormat.PCM_SIGNED.isCompatible(format)) {
return "audio/pcm";
}
throw new IllegalArgumentException("Invalid audio format given: " + format);
}
private String documentToString(Document document) throws TransformerException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
StringWriter out = new StringWriter();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(document), new StreamResult(out));
return out.toString();
}
private static void setAttributeIfNotNull(Element element, String name, Object value) {
if (value != null) {
element.setAttribute(name, value.toString());
}
}
}

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2024 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.onkyo.internal.handler;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
/**
* @author Krzysztof Goworek - Initial contribution
*/
class URIMetaDataProcessorTest {
private static final String MP3_STREAM_URL = "http://audio.server.local/audio.mp3";
private static final String AAC_STREAM_URL = "http://audio.server.local/audio.aac";
@Test
void generateShouldReturnEmptyStringWhenStreamIsNull() {
String result = new URIMetaDataProcessor().generate(MP3_STREAM_URL, null);
assertEquals("", result);
}
@Test
void generateShouldReturnEmptyStringWhenExceptionOccurs() {
AudioStream audioStream = new TestAudioStream(AudioFormat.MP3) {
@Override
public @Nullable String getId() {
throw new UnsupportedOperationException();
}
};
String result = new URIMetaDataProcessor().generate(MP3_STREAM_URL, audioStream);
assertEquals("", result);
}
@Test
void generateShouldReturnXMLWithGivenURLAndProtocolInfo() {
String result = new URIMetaDataProcessor().generate(MP3_STREAM_URL, new TestAudioStream(AudioFormat.MP3));
assertTrue(result.matches("<DIDL-Lite.+</DIDL-Lite>"));
assertTrue(result.contains(">" + MP3_STREAM_URL + "</res></item></DIDL-Lite>"));
assertTrue(result.contains("protocolInfo=\"http-get:*:audio/mpeg:*\""));
}
@Test
void generateShouldReturnXMLWithAllFormatAttributes() {
AudioFormat audioFormat = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, true, 8, 192000,
48000L, 2);
String result = new URIMetaDataProcessor().generate(AAC_STREAM_URL, new TestAudioStream(audioFormat));
assertTrue(result.contains("nrAudioChannels=\"2\""));
assertTrue(result.contains("sampleFrequency=\"48000\""));
assertTrue(result.contains("bitrate=\"192000\""));
assertTrue(result.contains("protocolInfo=\"http-get:*:audio/aac:*\""));
}
@NonNullByDefault
private static class TestAudioStream extends AudioStream {
private final AudioFormat audioFormat;
private TestAudioStream(AudioFormat audioFormat) {
this.audioFormat = audioFormat;
}
@Override
public AudioFormat getFormat() {
return audioFormat;
}
@Override
public int read() throws IOException {
throw new IOException("Unsupported operation");
}
}
}