[kodi] Support for more audio streams through the HTTP audio servlet (#15192)
* [kodi] Support for more audio streams through the HTTP audio servlet [kodi] Audio sink supporting more audio streams Related to #15113 --------- Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
parent
365e900a1f
commit
081bf3a9d4
|
@ -12,18 +12,21 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.kodi.internal;
|
package org.openhab.binding.kodi.internal;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNull;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.kodi.internal.handler.KodiHandler;
|
import org.openhab.binding.kodi.internal.handler.KodiHandler;
|
||||||
import org.openhab.core.audio.AudioFormat;
|
import org.openhab.core.audio.AudioFormat;
|
||||||
import org.openhab.core.audio.AudioHTTPServer;
|
import org.openhab.core.audio.AudioHTTPServer;
|
||||||
import org.openhab.core.audio.AudioSink;
|
import org.openhab.core.audio.AudioSink;
|
||||||
|
import org.openhab.core.audio.AudioSinkSync;
|
||||||
import org.openhab.core.audio.AudioStream;
|
import org.openhab.core.audio.AudioStream;
|
||||||
import org.openhab.core.audio.FixedLengthAudioStream;
|
import org.openhab.core.audio.StreamServed;
|
||||||
import org.openhab.core.audio.URLAudioStream;
|
import org.openhab.core.audio.URLAudioStream;
|
||||||
import org.openhab.core.audio.UnsupportedAudioFormatException;
|
import org.openhab.core.audio.UnsupportedAudioFormatException;
|
||||||
import org.openhab.core.audio.UnsupportedAudioStreamException;
|
import org.openhab.core.audio.UnsupportedAudioStreamException;
|
||||||
|
@ -39,16 +42,14 @@ import org.slf4j.LoggerFactory;
|
||||||
* @author Paul Frank - Adapted for Kodi
|
* @author Paul Frank - Adapted for Kodi
|
||||||
* @author Christoph Weitkamp - Improvements for playing audio notifications
|
* @author Christoph Weitkamp - Improvements for playing audio notifications
|
||||||
*/
|
*/
|
||||||
public class KodiAudioSink implements AudioSink {
|
public class KodiAudioSink extends AudioSinkSync {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(KodiAudioSink.class);
|
private final Logger logger = LoggerFactory.getLogger(KodiAudioSink.class);
|
||||||
|
|
||||||
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Collections
|
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
|
||||||
.unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
|
private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Set.of(AudioStream.class);
|
||||||
private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Collections
|
|
||||||
.unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet()));
|
|
||||||
// Needed because Kodi does multiple requests for the stream
|
// Needed because Kodi does multiple requests for the stream
|
||||||
private static final int STREAM_TIMEOUT = 30;
|
private static final int STREAM_TIMEOUT = 10;
|
||||||
|
|
||||||
private final KodiHandler handler;
|
private final KodiHandler handler;
|
||||||
private final AudioHTTPServer audioHTTPServer;
|
private final AudioHTTPServer audioHTTPServer;
|
||||||
|
@ -71,8 +72,30 @@ public class KodiAudioSink implements AudioSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(AudioStream audioStream)
|
public @NonNull CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
|
||||||
|
// we override this method to intercept URLAudioStream and handle it asynchronously. We won't wait for it to
|
||||||
|
// play through the end as it can be very long
|
||||||
|
if (audioStream instanceof URLAudioStream) {
|
||||||
|
// Asynchronous handling for URLAudioStream. Id it is an external URL, the speaker can access it itself and
|
||||||
|
// play it. There will be no volume restoration or call to dispose / complete, but there is no need to.
|
||||||
|
String url = ((URLAudioStream) audioStream).getURL();
|
||||||
|
AudioFormat format = audioStream.getFormat();
|
||||||
|
logger.trace("Processing audioStream URL {} of format {}.", url, format);
|
||||||
|
handler.playURI(new StringType(url));
|
||||||
|
tryClose(audioStream);
|
||||||
|
return new CompletableFuture<@Nullable Void>();
|
||||||
|
} else {
|
||||||
|
return super.processAndComplete(audioStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processSynchronously(AudioStream audioStream)
|
||||||
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
|
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
|
||||||
|
if (audioStream instanceof URLAudioStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (audioStream == null) {
|
if (audioStream == null) {
|
||||||
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
|
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
|
||||||
// stream.
|
// stream.
|
||||||
|
@ -81,28 +104,36 @@ public class KodiAudioSink implements AudioSink {
|
||||||
} else {
|
} else {
|
||||||
AudioFormat format = audioStream.getFormat();
|
AudioFormat format = audioStream.getFormat();
|
||||||
if (!AudioFormat.MP3.isCompatible(format) && !AudioFormat.WAV.isCompatible(format)) {
|
if (!AudioFormat.MP3.isCompatible(format) && !AudioFormat.WAV.isCompatible(format)) {
|
||||||
|
tryClose(audioStream);
|
||||||
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported.", format);
|
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported.", format);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioStream instanceof URLAudioStream) {
|
if (callbackUrl != null) {
|
||||||
// it is an external URL, the speaker can access it itself and play it
|
// we serve it on our own HTTP server for 10 seconds as Kodi requests the stream several times
|
||||||
String url = ((URLAudioStream) audioStream).getURL();
|
// Form the URL for streaming the notification from the OH web server
|
||||||
logger.trace("Processing audioStream URL {} of format {}.", url, format);
|
try {
|
||||||
handler.playURI(new StringType(url));
|
StreamServed streamServed = audioHTTPServer.serve(audioStream, STREAM_TIMEOUT, true);
|
||||||
} else if (audioStream instanceof FixedLengthAudioStream) {
|
String url = callbackUrl + streamServed.url();
|
||||||
if (callbackUrl != null) {
|
|
||||||
// we serve it on our own HTTP server for 30 seconds as Kodi requests the stream several times
|
|
||||||
// Form the URL for streaming the notification from the OH2 web server
|
|
||||||
String url = callbackUrl
|
|
||||||
+ audioHTTPServer.serve((FixedLengthAudioStream) audioStream, STREAM_TIMEOUT);
|
|
||||||
logger.trace("Processing audioStream URL {} of format {}.", url, format);
|
logger.trace("Processing audioStream URL {} of format {}.", url, format);
|
||||||
handler.playNotificationSoundURI(new StringType(url));
|
handler.playNotificationSoundURI(new StringType(url), false);
|
||||||
} else {
|
} catch (IOException e) {
|
||||||
logger.warn("We do not have any callback url, so Kodi cannot play the audio stream!");
|
tryClose(audioStream);
|
||||||
|
throw new UnsupportedAudioStreamException(
|
||||||
|
"Kodi binding was not able to handle the audio stream (cache on disk failed)",
|
||||||
|
audioStream.getClass(), e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedAudioStreamException(
|
tryClose(audioStream);
|
||||||
"Kodi can only handle URLAudioStream or FixedLengthAudioStreams.", audioStream.getClass());
|
logger.warn("We do not have any callback url, so Kodi cannot play the audio stream!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryClose(@Nullable InputStream is) {
|
||||||
|
if (is != null) {
|
||||||
|
try {
|
||||||
|
is.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
|
||||||
break;
|
break;
|
||||||
case CHANNEL_PLAYNOTIFICATION:
|
case CHANNEL_PLAYNOTIFICATION:
|
||||||
if (command instanceof StringType) {
|
if (command instanceof StringType) {
|
||||||
playNotificationSoundURI((StringType) command);
|
playNotificationSoundURI((StringType) command, true);
|
||||||
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
|
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
|
||||||
} else if (command.equals(RefreshType.REFRESH)) {
|
} else if (command.equals(RefreshType.REFRESH)) {
|
||||||
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
|
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
|
||||||
|
@ -456,12 +456,15 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
|
||||||
* Play the notification by 1) saving the state of the player, 2) stopping the current
|
* Play the notification by 1) saving the state of the player, 2) stopping the current
|
||||||
* playlist item, 3) adding the notification as a new playlist item, 4) playing the new
|
* playlist item, 3) adding the notification as a new playlist item, 4) playing the new
|
||||||
* playlist item, and 5) restoring the player to its previous state.
|
* playlist item, and 5) restoring the player to its previous state.
|
||||||
|
* set manageVolume to true if the binding must handle volume change by itself
|
||||||
*/
|
*/
|
||||||
public void playNotificationSoundURI(StringType uri) {
|
public void playNotificationSoundURI(StringType uri, boolean manageVolume) {
|
||||||
// save the current state of the player
|
// save the current state of the player
|
||||||
logger.trace("Saving current player state");
|
logger.trace("Saving current player state");
|
||||||
KodiPlayerState playerState = new KodiPlayerState();
|
KodiPlayerState playerState = new KodiPlayerState();
|
||||||
playerState.setSavedVolume(connection.getVolume());
|
if (manageVolume) {
|
||||||
|
playerState.setSavedVolume(connection.getVolume());
|
||||||
|
}
|
||||||
playerState.setPlaylistID(connection.getActivePlaylist());
|
playerState.setPlaylistID(connection.getActivePlaylist());
|
||||||
playerState.setSavedState(connection.getState());
|
playerState.setSavedState(connection.getState());
|
||||||
|
|
||||||
|
@ -482,10 +485,12 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
// set notification sound volume
|
// set notification sound volume
|
||||||
logger.trace("Setting up player for notification");
|
if (manageVolume) {
|
||||||
int notificationVolume = getNotificationSoundVolume().intValue();
|
logger.trace("Setting up player for notification");
|
||||||
connection.setVolume(notificationVolume);
|
int notificationVolume = getNotificationSoundVolume().intValue();
|
||||||
waitForVolume(notificationVolume);
|
connection.setVolume(notificationVolume);
|
||||||
|
waitForVolume(notificationVolume);
|
||||||
|
}
|
||||||
|
|
||||||
// add the notification uri to the playlist and play it
|
// add the notification uri to the playlist and play it
|
||||||
logger.trace("Playing notification");
|
logger.trace("Playing notification");
|
||||||
|
@ -504,8 +509,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
|
||||||
waitForPlaylistState(KodiPlaylistState.REMOVED);
|
waitForPlaylistState(KodiPlaylistState.REMOVED);
|
||||||
|
|
||||||
// restore previous volume
|
// restore previous volume
|
||||||
connection.setVolume(playerState.getSavedVolume());
|
if (manageVolume) {
|
||||||
waitForVolume(playerState.getSavedVolume());
|
connection.setVolume(playerState.getSavedVolume());
|
||||||
|
waitForVolume(playerState.getSavedVolume());
|
||||||
|
}
|
||||||
|
|
||||||
// resume playing save playlist item if player wasn't stopped
|
// resume playing save playlist item if player wasn't stopped
|
||||||
logger.trace("Restoring player state");
|
logger.trace("Restoring player state");
|
||||||
|
@ -551,10 +558,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
|
||||||
*/
|
*/
|
||||||
private boolean waitForState(KodiState state) {
|
private boolean waitForState(KodiState state) {
|
||||||
int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
|
int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
|
||||||
logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 100, state);
|
logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 1000, state);
|
||||||
while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
|
while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(100);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue