From b8a0a5bb45907494fd29c51ceb164e32b94e1e6f Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Sat, 27 May 2023 09:07:38 +0200 Subject: [PATCH] [audio] More capabilities for AudioSink using the AudioServlet Applying code review advices (CompletableFuture and others) Signed-off-by: Gwendal Roulleau --- .../openhab/core/audio/AudioHTTPServer.java | 44 ++++---- .../org/openhab/core/audio/AudioManager.java | 11 ++ .../org/openhab/core/audio/AudioSink.java | 27 ++--- .../openhab/core/audio/AudioSinkAsync.java | 65 ++++++++--- .../org/openhab/core/audio/AudioSinkSync.java | 41 +++++-- .../org/openhab/core/audio/StreamServed.java | 30 +++++ .../core/audio/internal/AudioManagerImpl.java | 54 ++++++++- .../core/audio/internal/AudioServlet.java | 104 +++++++++--------- .../javasound/JavaSoundAudioSink.java | 29 ++--- .../internal/webaudio/WebAudioAudioSink.java | 10 +- .../core/audio/utils/AudioSinkUtils.java | 64 ++--------- .../core/audio/utils/AudioSinkUtilsImpl.java | 91 +++++++++++++++ .../core/audio/utils/AudioStreamUtils.java | 66 ----------- .../internal/AbstractAudioServletTest.java | 5 +- .../core/audio/internal/AudioServletTest.java | 2 +- .../core/voice/internal/VoiceManagerImpl.java | 11 +- 16 files changed, 393 insertions(+), 261 deletions(-) create mode 100644 bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java create mode 100644 bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java index f8379a0ae0d..2cb85ee5833 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioHTTPServer.java @@ -13,6 +13,7 @@ package org.openhab.core.audio; import java.io.IOException; +import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.audio.internal.AudioServlet; @@ -36,43 +37,48 @@ public interface AudioHTTPServer { * * @param stream the stream to serve on HTTP * @return the relative URL to access the stream starting with a '/' + * @deprecated Use {@link AudioHTTPServer#serve(AudioStream, int, boolean, CompletableFuture)} */ + @Deprecated String serve(AudioStream stream); /** * Creates a relative url for a given {@link AudioStream} where it can be requested multiple times within the given * time frame. - * This method accepts all {@link AudioStream}s, but it is better to use {@link ClonableAudioStream}s since it - * needs to be able to create multiple concurrent streams from it. - * If generic {@link AudioStream} is used, the method tries to add the Clonable capability by storing it in a small - * memory buffer, - * e.g {@link ByteArrayAudioStream}, or in a cached file if the stream reached the buffer capacity, or fails if the - * stream is too long. + * This method accepts all {@link AudioStream}s, but it is better to use {@link ClonableAudioStream}s. If generic + * {@link AudioStream} is used, the method tries to add the Clonable capability by storing it in a small memory + * buffer, e.g {@link ByteArrayAudioStream}, or in a cached file if the stream reached the buffer capacity, + * or fails if the stream is too long. * Streams are closed, once they expire. * * @param stream the stream to serve on HTTP * @param seconds number of seconds for which the stream is available through HTTP * @return the relative URL to access the stream starting with a '/' + * @deprecated Use {@link AudioHTTPServer#serve(AudioStream, int, boolean, CompletableFuture)} */ + @Deprecated String serve(AudioStream stream, int seconds); /** - * Creates a relative url for a given {@link AudioStream} where it can be requested multiple times within the given - * time frame. - * This method accepts all {@link AudioStream}s, but it is better to use {@link ClonableAudioStream}s since it - * needs to be able to create multiple concurrent streams from it. - * If generic {@link AudioStream} is used, method tries to add the Clonable capability by storing it in a small - * memory buffer, - * e.g {@link ByteArrayAudioStream}, or in a cached file if the stream reached the buffer capacity, or fails if the - * stream is too long. + * Creates a relative url for a given {@link AudioStream} where it can be requested one or multiple times within the + * given time frame. + * This method accepts all {@link AudioStream}s, but if multiTimeStream is set to true it is better to use + * {@link ClonableAudioStream}s. Otherwise, if a generic {@link AudioStream} is used, the method will then try + * to add the Clonable capability by storing it in a small memory buffer, e.g {@link ByteArrayAudioStream}, or in a + * cached file if the stream reached the buffer capacity, or fails to render the sound completely if the stream is + * too long. + * A {@link CompletableFuture} is used to inform the caller that the playback ends in order to clean + * resources and run delayed task, such as restoring volume. * Streams are closed, once they expire. * * @param stream the stream to serve on HTTP - * @param seconds number of seconds for which the stream is available through HTTP - * @param a Runnable callback for cleaning resources. The AudioHTTPServer will run the callback when the stream is - * not used anymore and timed-out. - * @return the relative URL to access the stream starting with a '/' + * @param seconds number of seconds for which the stream is available through HTTP. The stream will be deleted only + * if not started, so you can set a duration shorter than the track's duration. + * @param multiTimeStream set to true if this stream should be played multiple time, and thus needs to be made + * Cloneable if it is not already. + * @return information about the {@link StreamServed}, including the relative URL to access the stream starting with + * a '/', and a CompletableFuture to know when the playback ends. * @throws IOException when the stream is not a {@link ClonableAudioStream} and we cannot get or store it on disk. */ - String serve(AudioStream stream, int seconds, Runnable callBack) throws IOException; + StreamServed serve(AudioStream stream, int seconds, boolean multiTimeStream) throws IOException; } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java index a60e5fe4172..d640c0f3996 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioManager.java @@ -252,4 +252,15 @@ public interface AudioManager { * @return ids of matching sinks */ Set getSinkIds(String pattern); + + /** + * Handles a volume command change and returns a Runnable to restore it. + * Returning a Runnable allows us to have a no-op Runnable if changing volume back is not needed, and conveniently + * keeping it as one liner usable in a chain for the caller. + * + * @param volume The volume to set + * @param sink The sink to set the volume to + * @return A runnable to restore the volume to its previous value, or no-operation if no change is required. + */ + Runnable handleVolumeCommand(@Nullable PercentType volume, AudioSink sink); } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java index 0dd90dd58a8..d3f40e601f8 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSink.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.util.Locale; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -58,19 +59,21 @@ public interface AudioSink { * * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream. * - * When the process method ends, if the stream implements the {@link org.openhab.core.common.Disposable} interface, - * the sink should hereafter get rid of it by calling the dispose method. + * When the stream is not needed anymore, if the stream implements the {@link org.openhab.core.common.Disposable} + * interface, the sink should hereafter get rid of it by calling the dispose method. * * @param audioStream the audio stream to play or null to keep quiet * @throws UnsupportedAudioFormatException If audioStream format is not supported * @throws UnsupportedAudioStreamException If audioStream is not supported + * @deprecated Use {@link AudioSink#processAndComplete(AudioStream)} */ + @Deprecated void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException; /** - * Processes the passed {@link AudioStream}, and executes the whenFinished Runnable (or stores for later execution - * if the sink is asynchronous). + * Processes the passed {@link AudioStream}, and returns a CompletableFuture that should complete when the sound is + * fully played. It is the sink responsibility to complete this future. * * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException} * is thrown. @@ -80,23 +83,17 @@ void process(@Nullable AudioStream audioStream) * * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream. * - * When the process method ends, if the stream implements the {@link org.openhab.core.common.Disposable} interface, - * the sink should hereafter get rid of it by calling the dispose method. + * When the stream is not needed anymore, if the stream implements the {@link org.openhab.core.common.Disposable} + * interface, the sink should hereafter get rid of it by calling the dispose method. * * @param audioStream the audio stream to play or null to keep quiet - * @param whenFinished A Runnable to run when the sound is finished playing. * @throws UnsupportedAudioFormatException If audioStream format is not supported * @throws UnsupportedAudioStreamException If audioStream is not supported */ - default void process(@Nullable AudioStream audioStream, @Nullable Runnable whenFinished) + default CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { - try { - process(audioStream); - } finally { - if (whenFinished != null) { - whenFinished.run(); - } - } + process(audioStream); + return CompletableFuture.completedFuture(null); } /** diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java index 2f00f19f357..cbc2962abe6 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkAsync.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,10 +26,10 @@ /** * Definition of an audio output like headphones, a speaker or for writing to * a file / clip. - * This version is asynchronous: when the process() method returns, the {@link AudioStream} - * may or may not be played, and we don't know when the delayed task will be executed. - * CAUTION : It is the responsibility of the implementing AudioSink class to call the runDelayedTask - * method when playing is done. + * Helper class for asynchronous sink : when the process() method returns, the {@link AudioStream} + * may or may not be played. It is the responsibility of the implementing AudioSink class to + * complete the CompletableFuture when playing is done. Any delayed tasks will then be performed, such as volume + * restoration. * * @author Gwendal Roulleau - Initial contribution */ @@ -37,38 +38,66 @@ public abstract class AudioSinkAsync implements AudioSink { private final Logger logger = LoggerFactory.getLogger(AudioSinkAsync.class); - private final Map runnableByAudioStream = new HashMap<>(); + private final Map> runnableByAudioStream = new HashMap<>(); @Override - public void process(@Nullable AudioStream audioStream, @Nullable Runnable whenFinished) + public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + CompletableFuture<@Nullable Void> completableFuture = new CompletableFuture<@Nullable Void>(); try { - if (audioStream != null && whenFinished != null) { - runnableByAudioStream.put(audioStream, whenFinished); + if (audioStream != null) { + runnableByAudioStream.put(audioStream, completableFuture); } - process(audioStream); + processAsynchronously(audioStream); + return completableFuture; } finally { - if (audioStream == null && whenFinished != null) { + if (audioStream == null) { // No need to delay the post process task - whenFinished.run(); + runnableByAudioStream.remove(audioStream); + completableFuture.complete(null); } } } + @Override + public void process(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + processAsynchronously(audioStream); + } + /** - * Will run the delayed task stored previously. + * Processes the passed {@link AudioStream} asynchronously. This method is expected to return before the stream is + * fully played. This is the sink responsibility to call the {@link AudioSinkAsync#playbackFinished(AudioStream)} + * when it is. + * + * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException} + * is thrown. * - * @param audioStream The AudioStream is the key to find the delayed Runnable task in the storage. + * If the passed {@link AudioStream} has an {@link AudioFormat} not supported by this instance, + * an {@link UnsupportedAudioFormatException} is thrown. + * + * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream. + * + * @param audioStream the audio stream to play or null to keep quiet + * @throws UnsupportedAudioFormatException If audioStream format is not supported + * @throws UnsupportedAudioStreamException If audioStream is not supported */ - protected void runDelayed(AudioStream audioStream) { - Runnable delayedTask = runnableByAudioStream.remove(audioStream); + protected abstract void processAsynchronously(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException; - if (delayedTask != null) { - delayedTask.run(); + /** + * Will complete the future previously returned, allowing the core to run delayed task. + * + * @param audioStream The AudioStream is the key to find the delayed CompletableFuture in the storage. + */ + protected void playbackFinished(AudioStream audioStream) { + CompletableFuture<@Nullable Void> completableFuture = runnableByAudioStream.remove(audioStream); + if (completableFuture != null) { + completableFuture.complete(null); } // if the stream is not needed anymore, then we should call back the AudioStream to let it a chance - // to auto dispose: + // to auto dispose. if (audioStream instanceof Disposable disposableAudioStream) { try { disposableAudioStream.dispose(); diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java index cc6105bd0ca..e0318385083 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/AudioSinkSync.java @@ -13,6 +13,7 @@ package org.openhab.core.audio; import java.io.IOException; +import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -23,9 +24,9 @@ /** * Definition of an audio output like headphones, a speaker or for writing to * a file / clip. - * This version is synchronous: when the process() method returns, + * Helper class for synchronous sink : when the process() method returns, * the source is considered played, and could be disposed. - * Any delayed tasks can then be performed, such as volume restoration + * Any delayed tasks can then be performed, such as volume restoration. * * @author Gwendal Roulleau - Initial contribution */ @@ -35,17 +36,12 @@ public abstract class AudioSinkSync implements AudioSink { private final Logger logger = LoggerFactory.getLogger(AudioSinkSync.class); @Override - public void process(@Nullable AudioStream audioStream, @Nullable Runnable whenFinished) + public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { try { - process(audioStream); + processSynchronously(audioStream); } finally { - if (whenFinished != null) { - whenFinished.run(); - } - - // if the stream is not needed anymore, then we should call back the AudioStream to let it a chance - // to auto dispose: + // as the stream is not needed anymore, we should dispose of it if (audioStream instanceof Disposable disposableAudioStream) { try { disposableAudioStream.dispose(); @@ -59,5 +55,30 @@ public void process(@Nullable AudioStream audioStream, @Nullable Runnable whenFi } } } + return CompletableFuture.completedFuture(null); } + + @Override + public void process(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + processSynchronously(audioStream); + } + + /** + * Processes the passed {@link AudioStream} and returns only when the playback is ended. + * + * If the passed {@link AudioStream} is not supported by this instance, an {@link UnsupportedAudioStreamException} + * is thrown. + * + * If the passed {@link AudioStream} has an {@link AudioFormat} not supported by this instance, + * an {@link UnsupportedAudioFormatException} is thrown. + * + * In case the audioStream is null, this should be interpreted as a request to end any currently playing stream. + * + * @param audioStream the audio stream to play or null to keep quiet + * @throws UnsupportedAudioFormatException If audioStream format is not supported + * @throws UnsupportedAudioStreamException If audioStream is not supported + */ + protected abstract void processSynchronously(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException; } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java new file mode 100644 index 00000000000..35bf5946957 --- /dev/null +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/StreamServed.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2023 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.core.audio; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Streams served by the AudioHTTPServer. + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public record StreamServed(String url, AudioStream audioStream, AtomicInteger currentlyServedStream, AtomicLong timeout, + boolean multiTimeStream, CompletableFuture<@Nullable Void> playEnd) { +} diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java index db075890576..066d1edbf1f 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioManagerImpl.java @@ -40,7 +40,6 @@ import org.openhab.core.audio.URLAudioStream; import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioStreamException; -import org.openhab.core.audio.utils.AudioSinkUtils; import org.openhab.core.audio.utils.ToneSynthesizer; import org.openhab.core.config.core.ConfigOptionProvider; import org.openhab.core.config.core.ConfigurableService; @@ -123,10 +122,12 @@ public void play(@Nullable AudioStream audioStream, @Nullable String sinkId) { public void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume) { AudioSink sink = getSink(sinkId); if (sink != null) { - Runnable volumeRestoration = AudioSinkUtils.handleVolumeCommand(volume, sink, logger); + Runnable volumeRestauration = null; try { - sink.process(audioStream, volumeRestoration); + volumeRestauration = handleVolumeCommand(volume, sink); + sink.processAndComplete(audioStream).thenRun(volumeRestauration); } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { + volumeRestauration.run(); logger.warn("Error playing '{}': {}", audioStream, e.getMessage(), e); } } else { @@ -325,6 +326,53 @@ public Set getSinkIds(String pattern) { return null; } + @Override + public Runnable handleVolumeCommand(@Nullable PercentType volume, AudioSink sink) { + boolean volumeChanged = false; + PercentType oldVolume = null; + + Runnable toRunWhenProcessFinished = () -> { + }; + + if (volume == null) { + return toRunWhenProcessFinished; + } + + // set notification sound volume + try { + // get current volume + oldVolume = sink.getVolume(); + } catch (IOException | UnsupportedOperationException e) { + logger.debug("An exception occurred while getting the volume of sink '{}' : {}", sink.getId(), + e.getMessage(), e); + } + + if (!volume.equals(oldVolume) || oldVolume == null) { + try { + sink.setVolume(volume); + volumeChanged = true; + } catch (IOException | UnsupportedOperationException e) { + logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(), + e.getMessage(), e); + } + } + + final PercentType oldVolumeFinal = oldVolume; + // restore volume only if it was set before + if (volumeChanged && oldVolumeFinal != null) { + toRunWhenProcessFinished = () -> { + try { + sink.setVolume(oldVolumeFinal); + } catch (IOException | UnsupportedOperationException e) { + logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(), + e.getMessage(), e); + } + }; + } + + return toRunWhenProcessFinished; + } + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addAudioSource(AudioSource audioSource) { this.audioSources.put(audioSource.getId(), audioSource); diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java index 0452f2f9b04..e7acbb867ee 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/AudioServlet.java @@ -23,6 +23,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -48,10 +49,13 @@ import org.openhab.core.audio.ClonableAudioStream; import org.openhab.core.audio.FileAudioStream; import org.openhab.core.audio.FixedLengthAudioStream; -import org.openhab.core.audio.utils.AudioStreamUtils; +import org.openhab.core.audio.StreamServed; +import org.openhab.core.audio.utils.AudioSinkUtils; import org.openhab.core.common.ThreadPoolManager; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName; import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern; import org.slf4j.Logger; @@ -88,9 +92,17 @@ public class AudioServlet extends HttpServlet implements AudioHTTPServer { @Nullable ScheduledFuture periodicCleaner; + private AudioSinkUtils audioSinkUtils; + + @Activate + public AudioServlet(@Reference AudioSinkUtils audioSinkUtils) { + super(); + this.audioSinkUtils = audioSinkUtils; + } + @Deactivate protected synchronized void deactivate() { - servedStreams.values().stream().map(streamServed -> streamServed.audioStream).forEach(this::tryClose); + servedStreams.values().stream().map(streamServed -> streamServed.audioStream()).forEach(this::tryClose); servedStreams.clear(); } @@ -103,17 +115,17 @@ private void tryClose(@Nullable AudioStream stream) { } } - private InputStream prepareInputStream(final StreamServed servedStream, final HttpServletResponse resp, + private InputStream prepareInputStream(final StreamServed streamServed, final HttpServletResponse resp, List acceptedMimeTypes) throws AudioException { - logger.debug("Stream to serve is {}", servedStream.id); + logger.debug("Stream to serve is {}", streamServed.url()); // try to set the content-type, if possible final String mimeType; - if (AudioFormat.CODEC_MP3.equals(servedStream.audioStream.getFormat().getCodec())) { + if (AudioFormat.CODEC_MP3.equals(streamServed.audioStream().getFormat().getCodec())) { mimeType = "audio/mpeg"; - } else if (AudioFormat.CONTAINER_WAVE.equals(servedStream.audioStream.getFormat().getContainer())) { + } else if (AudioFormat.CONTAINER_WAVE.equals(streamServed.audioStream().getFormat().getContainer())) { mimeType = WAV_MIME_TYPES.stream().filter(acceptedMimeTypes::contains).findFirst().orElse("audio/wav"); - } else if (AudioFormat.CONTAINER_OGG.equals(servedStream.audioStream.getFormat().getContainer())) { + } else if (AudioFormat.CONTAINER_OGG.equals(streamServed.audioStream().getFormat().getContainer())) { mimeType = "audio/ogg"; } else { mimeType = null; @@ -123,17 +135,17 @@ private InputStream prepareInputStream(final StreamServed servedStream, final Ht } // try to set the content-length, if possible - if (servedStream.audioStream instanceof FixedLengthAudioStream fixedLengthServedStream) { + if (streamServed.audioStream() instanceof FixedLengthAudioStream fixedLengthServedStream) { final long size = fixedLengthServedStream.length(); resp.setContentLength((int) size); } - if (servedStream.multiTimeStream - && servedStream.audioStream instanceof ClonableAudioStream clonableAudioStream) { + if (streamServed.multiTimeStream() + && streamServed.audioStream() instanceof ClonableAudioStream clonableAudioStream) { // we need to care about concurrent access and have a separate stream for each thread return clonableAudioStream.getClonedStream(); } else { - return servedStream.audioStream; + return streamServed.audioStream(); } } @@ -169,14 +181,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } // we count the number of active process using the input stream - AtomicInteger currentlyServedStream = servedStream.currentlyServedStream; - if (currentlyServedStream.incrementAndGet() == 1 || servedStream.multiTimeStream) { + AtomicInteger currentlyServedStream = servedStream.currentlyServedStream(); + if (currentlyServedStream.incrementAndGet() == 1 || servedStream.multiTimeStream()) { try (final InputStream stream = prepareInputStream(servedStream, resp, acceptedMimeTypes)) { - Long endOfPlayTimestamp = AudioStreamUtils.transferAndAnalyzeLength(stream, resp.getOutputStream(), - servedStream.audioStream.getFormat()); + Long endOfPlayTimestamp = audioSinkUtils.transferAndAnalyzeLength(stream, resp.getOutputStream(), + servedStream.audioStream().getFormat()); // update timeout with the sound duration : if (endOfPlayTimestamp != null) { - servedStream.timeout.set(Math.max(servedStream.timeout.get(), endOfPlayTimestamp)); + servedStream.timeout().set(Math.max(servedStream.timeout().get(), endOfPlayTimestamp)); } resp.flushBuffer(); } catch (final AudioException ex) { @@ -191,12 +203,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } // we can immediately dispose and remove, if it is a one time stream - if (!servedStream.multiTimeStream) { - Runnable callback = servedStream.callback; - if (callback != null) { - callback.run(); - } + if (!servedStream.multiTimeStream()) { servedStreams.remove(streamId); + servedStream.playEnd().complete(null); logger.debug("Removed timed out stream {}", streamId); } } @@ -205,19 +214,16 @@ private synchronized void removeTimedOutStreams() { // Build list of expired streams. long now = System.nanoTime(); final List toRemove = servedStreams.entrySet().stream() - .filter(e -> e.getValue().timeout.get() < now && e.getValue().currentlyServedStream.get() <= 0) + .filter(e -> e.getValue().timeout().get() < now && e.getValue().currentlyServedStream().get() <= 0) .map(Entry::getKey).collect(Collectors.toList()); toRemove.forEach(streamId -> { // the stream has expired and no one is using it, we need to remove it! StreamServed streamServed = servedStreams.remove(streamId); if (streamServed != null) { - tryClose(streamServed.audioStream); + tryClose(streamServed.audioStream()); // we can notify the caller of the stream consumption - Runnable callback = streamServed.callback; - if (callback != null) { - callback.run(); - } + streamServed.playEnd().complete(null); logger.debug("Removed timed out stream {}", streamId); } }); @@ -239,24 +245,20 @@ private synchronized void removeTimedOutStreams() { @Override public String serve(AudioStream stream) { - String streamId = UUID.randomUUID().toString(); - // Because we cannot wait indefinitely before executing the callback, - // even if this is a one time stream, we set a timeout just in case. - long timeOut = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); - StreamServed streamToServe = new StreamServed(streamId, stream, new AtomicInteger(), new AtomicLong(timeOut), - false, null); - servedStreams.put(streamId, streamToServe); - - // try to clean, or a least launch the periodic cleanse: - removeTimedOutStreams(); - - return getRelativeURL(streamId); + try { + // In case the stream is never played, we cannot wait indefinitely before executing the callback. + // so we set a timeout (even if this is a one time stream). + return serve(stream, 10, false).url(); + } catch (IOException e) { + logger.warn("Cannot precache the audio stream to serve it", e); + return getRelativeURL("error"); + } } @Override public String serve(AudioStream stream, int seconds) { try { - return serve(stream, seconds, null); + return serve(stream, seconds, true).url(); } catch (IOException e) { logger.warn("Cannot precache the audio stream to serve it", e); return getRelativeURL("error"); @@ -264,23 +266,23 @@ public String serve(AudioStream stream, int seconds) { } @Override - public String serve(AudioStream stream, int seconds, @Nullable Runnable callBack) throws IOException { + public StreamServed serve(AudioStream originalStream, int seconds, boolean multiTimeStream) throws IOException { String streamId = UUID.randomUUID().toString(); - ClonableAudioStream clonableAudioStream = null; - if (stream instanceof ClonableAudioStream clonableAudioStreamDetected) { - clonableAudioStream = clonableAudioStreamDetected; - } else { // we prefer a ClonableAudioStream, but we can try to make one - clonableAudioStream = createClonableInputStream(stream, streamId); + AudioStream audioStream = originalStream; + if (!(originalStream instanceof ClonableAudioStream) && multiTimeStream) { + // we we can try to make a Cloneable stream as it is needed + audioStream = createClonableInputStream(originalStream, streamId); } long timeOut = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds); - StreamServed streamToServe = new StreamServed(streamId, clonableAudioStream, new AtomicInteger(), - new AtomicLong(timeOut), true, callBack); + CompletableFuture<@Nullable Void> playEnd = new CompletableFuture<@Nullable Void>(); + StreamServed streamToServe = new StreamServed(getRelativeURL(streamId), audioStream, new AtomicInteger(), + new AtomicLong(timeOut), multiTimeStream, playEnd); servedStreams.put(streamId, streamToServe); // try to clean, or a least launch the periodic cleanse: removeTimedOutStreams(); - return getRelativeURL(streamId); + return streamToServe; } private ClonableAudioStream createClonableInputStream(AudioStream stream, String streamId) throws IOException { @@ -324,8 +326,4 @@ Map getServedStreams() { private String getRelativeURL(String streamId) { return SERVLET_PATH + "/" + streamId; } - - protected static record StreamServed(String id, AudioStream audioStream, AtomicInteger currentlyServedStream, - AtomicLong timeout, boolean multiTimeStream, @Nullable Runnable callback) { - } } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java index e9fca650224..16e8b36789d 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/javasound/JavaSoundAudioSink.java @@ -80,7 +80,7 @@ protected void activate(BundleContext context) { } @Override - public synchronized void process(final @Nullable AudioStream audioStream) + public synchronized void processAsynchronously(final @Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { if (audioStream != null && !AudioFormat.CODEC_MP3.equals(audioStream.getFormat().getCodec())) { AudioPlayer audioPlayer = new AudioPlayer(audioStream); @@ -104,8 +104,7 @@ public synchronized void process(final @Nullable AudioStream audioStream) } else { try { // we start a new continuous stream and store its handle - streamPlayer = new Player(audioStream); - playInThread(streamPlayer, () -> runDelayed(audioStream)); + playInThread(audioStream, true); } catch (JavaLayerException e) { LOGGER.error("An exception occurred while playing url audio stream : '{}'", e.getMessage()); } @@ -114,7 +113,7 @@ public synchronized void process(final @Nullable AudioStream audioStream) } else { // we are playing some normal file (no url stream) try { - playInThread(new Player(audioStream), () -> runDelayed(audioStream)); + playInThread(audioStream, false); } catch (JavaLayerException e) { LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage()); } @@ -122,18 +121,20 @@ public synchronized void process(final @Nullable AudioStream audioStream) } } - private void playInThread(final @Nullable Player player, Runnable toRunAfter) { + private void playInThread(final AudioStream audioStream, boolean store) throws JavaLayerException { // run in new thread + Player streamPlayerFinal = new Player(audioStream); + if (store) { // we store its handle in case we want to interrupt it. + streamPlayer = streamPlayerFinal; + } threadFactory.newThread(() -> { - if (player != null) { - try { - player.play(); - } catch (Exception e) { - LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage()); - } finally { - player.close(); - toRunAfter.run(); - } + try { + streamPlayerFinal.play(); + } catch (Exception e) { + LOGGER.error("An exception occurred while playing audio : '{}'", e.getMessage()); + } finally { + streamPlayerFinal.close(); + playbackFinished(audioStream); } }).start(); } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java index 270103e0ce1..36e492ee772 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/internal/webaudio/WebAudioAudioSink.java @@ -23,6 +23,7 @@ import org.openhab.core.audio.AudioSink; import org.openhab.core.audio.AudioSinkAsync; import org.openhab.core.audio.AudioStream; +import org.openhab.core.audio.StreamServed; import org.openhab.core.audio.URLAudioStream; import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioStreamException; @@ -61,7 +62,7 @@ public WebAudioAudioSink(@Reference AudioHTTPServer audioHTTPServer, @Reference } @Override - public void process(@Nullable AudioStream audioStream) + public void processAsynchronously(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { if (audioStream == null) { // in case the audioStream is null, this should be interpreted as a request to end any currently playing @@ -80,11 +81,12 @@ public void process(@Nullable AudioStream audioStream) logger.debug("Error while closing the audio stream: {}", e.getMessage(), e); } } else { - // we will let the HTTP servlet run the delayed task when finished with the stream - Runnable delayedTask = () -> this.runDelayed(audioStream); // we need to serve it for a while and make it available to multiple clients try { - sendEvent(audioHTTPServer.serve(audioStream, 10, delayedTask).toString()); + StreamServed servedStream = audioHTTPServer.serve(audioStream, 10, true); + // we will let the HTTP servlet run the delayed task when finished with the stream + servedStream.playEnd().thenRun(() -> this.playbackFinished(audioStream)); + sendEvent(servedStream.url()); } catch (IOException e) { logger.warn("Cannot precache the audio stream to serve it", e); } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java index 84f7f21d300..b5a96e10811 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtils.java @@ -13,12 +13,12 @@ package org.openhab.core.audio.utils; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.audio.AudioSink; -import org.openhab.core.library.types.PercentType; -import org.slf4j.Logger; +import org.openhab.core.audio.AudioFormat; /** * Some utility methods for sink @@ -27,57 +27,17 @@ * */ @NonNullByDefault -public class AudioSinkUtils { +public interface AudioSinkUtils { /** - * Handle a volume command change and returns a Runnable to restore it. + * Transfers data from an input stream to an output stream and computes on the fly its duration * - * @param volume The volume to set - * @param sink The sink to set the volume to - * @param logger to log error to - * @return A runnable to restore the volume to its previous value, or null if no change is required + * @param in the input stream giving audio data ta play + * @param out the output stream receiving data to play + * @return the timestamp (from System.nanoTime) when the sound should be fully played. Returns null if computing + * time fails. + * @throws IOException if reading from the stream or writing to the stream failed */ - public static @Nullable Runnable handleVolumeCommand(@Nullable PercentType volume, AudioSink sink, Logger logger) { - boolean volumeChanged = false; - PercentType oldVolume = null; - - if (volume == null) { - return null; - } - - // set notification sound volume - try { - // get current volume - oldVolume = sink.getVolume(); - } catch (IOException | UnsupportedOperationException e) { - logger.debug("An exception occurred while getting the volume of sink '{}' : {}", sink.getId(), - e.getMessage(), e); - } - - if (!volume.equals(oldVolume) || oldVolume == null) { - try { - sink.setVolume(volume); - volumeChanged = true; - } catch (IOException | UnsupportedOperationException e) { - logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(), - e.getMessage(), e); - } - } - - final PercentType oldVolumeFinal = oldVolume; - Runnable toRunWhenProcessFinished = null; - // restore volume only if it was set before - if (volumeChanged && oldVolumeFinal != null) { - toRunWhenProcessFinished = () -> { - try { - sink.setVolume(oldVolumeFinal); - } catch (IOException | UnsupportedOperationException e) { - logger.debug("An exception occurred while setting the volume of sink '{}' : {}", sink.getId(), - e.getMessage(), e); - } - }; - } - - return toRunWhenProcessFinished; - } + @Nullable + Long transferAndAnalyzeLength(InputStream in, OutputStream out, AudioFormat audioFormat) throws IOException; } diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java new file mode 100644 index 00000000000..f49da0ce8e0 --- /dev/null +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioSinkUtilsImpl.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2023 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.core.audio.utils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javazoom.jl.decoder.Bitstream; +import javazoom.jl.decoder.BitstreamException; +import javazoom.jl.decoder.Header; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.audio.AudioFormat; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Some utility methods for sink + * + * @author Gwendal Roulleau - Initial contribution + * + */ +@NonNullByDefault +@Component +public class AudioSinkUtilsImpl implements AudioSinkUtils { + + private final Logger logger = LoggerFactory.getLogger(AudioSinkUtilsImpl.class); + + @Override + public @Nullable Long transferAndAnalyzeLength(InputStream in, OutputStream out, AudioFormat audioFormat) + throws IOException { + // take some data from the stream beginning + byte[] dataBytes = in.readNBytes(8192); + + // beginning sound timestamp : + long startTime = System.nanoTime(); + // copy already read data to the output stream : + out.write(dataBytes); + // transfer everything else + Long dataTransferedLength = dataBytes.length + in.transferTo(out); + + if (dataTransferedLength > 0) { + if (AudioFormat.CODEC_PCM_SIGNED.equals(audioFormat.getCodec())) { + try (AudioInputStream audioInputStream = AudioSystem + .getAudioInputStream(new ByteArrayInputStream(dataBytes))) { + int frameSize = audioInputStream.getFormat().getFrameSize(); + float frameRate = audioInputStream.getFormat().getFrameRate(); + long computedDuration = Float.valueOf((dataTransferedLength / (frameSize * frameRate)) * 1000000000) + .longValue(); + return startTime + computedDuration; + } catch (IOException | UnsupportedAudioFileException e) { + logger.debug("Cannot compute the duration of input stream", e); + return null; + } + } else if (AudioFormat.CODEC_MP3.equals(audioFormat.getCodec())) { + // not precise, no VBR, but better than nothing + Bitstream bitstream = new Bitstream(new ByteArrayInputStream(dataBytes)); + try { + Header h = bitstream.readFrame(); + if (h != null) { + long computedDuration = Float.valueOf(h.total_ms(dataTransferedLength.intValue()) * 1000000) + .longValue(); + return startTime + computedDuration; + } + } catch (BitstreamException ex) { + logger.debug("Cannot compute the duration of input stream", ex); + return null; + } + } + } + + return null; + } +} diff --git a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioStreamUtils.java b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioStreamUtils.java index be7402cebc7..04f6a80fe36 100644 --- a/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioStreamUtils.java +++ b/bundles/org.openhab.core.audio/src/main/java/org/openhab/core/audio/utils/AudioStreamUtils.java @@ -12,21 +12,7 @@ */ package org.openhab.core.audio.utils; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import javazoom.jl.decoder.Bitstream; -import javazoom.jl.decoder.BitstreamException; -import javazoom.jl.decoder.Header; - -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.UnsupportedAudioFileException; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.audio.AudioFormat; /** * Some general filename and extension utilities. @@ -80,56 +66,4 @@ public static String getExtension(String filename) { public static boolean isExtension(String filename, String extension) { return !extension.isEmpty() && getExtension(filename).equals(extension); } - - /** - * Transfers data from an input stream to an output stream and computes on the fly its duration - * - * @param in the input stream giving audio data ta play - * @param out the output stream receiving data to play - * @return the timestamp (from System.nanoTime) when the sound should be fully played. Returns null if computing - * time fails. - * @throws IOException if reading from the stream or writing to the stream failed - */ - public static @Nullable Long transferAndAnalyzeLength(InputStream in, OutputStream out, AudioFormat audioFormat) - throws IOException { - // take some data from the stream beginning - byte[] dataBytes = in.readNBytes(8192); - - // beginning sound timestamp : - long startTime = System.nanoTime(); - // copy already read data to the output stream : - out.write(dataBytes); - // transfer everything else - Long dataTransferedLength = dataBytes.length + in.transferTo(out); - - if (dataTransferedLength > 0) { - if (AudioFormat.CODEC_PCM_SIGNED.equals(audioFormat.getCodec())) { - try (AudioInputStream audioInputStream = AudioSystem - .getAudioInputStream(new ByteArrayInputStream(dataBytes))) { - int frameSize = audioInputStream.getFormat().getFrameSize(); - float frameRate = audioInputStream.getFormat().getFrameRate(); - long computedDuration = Float.valueOf((dataTransferedLength / (frameSize * frameRate)) * 1000000000) - .longValue(); - return startTime + computedDuration; - } catch (IOException | UnsupportedAudioFileException e) { - return null; - } - } else if (AudioFormat.CODEC_MP3.equals(audioFormat.getCodec())) { - // not precise, no VBR, but better than nothing - Bitstream bitstream = new Bitstream(new ByteArrayInputStream(dataBytes)); - try { - Header h = bitstream.readFrame(); - if (h != null) { - long computedDuration = Float.valueOf(h.total_ms(dataTransferedLength.intValue()) * 1000000) - .longValue(); - return startTime + computedDuration; - } - } catch (BitstreamException ex) { - return null; - } - } - } - - return null; - } } diff --git a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java index 458b6e47cf3..c70545adc4e 100644 --- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java +++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AbstractAudioServletTest.java @@ -33,6 +33,8 @@ import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioStream; import org.openhab.core.audio.ByteArrayAudioStream; +import org.openhab.core.audio.utils.AudioSinkUtils; +import org.openhab.core.audio.utils.AudioSinkUtilsImpl; import org.openhab.core.test.TestPortUtil; import org.openhab.core.test.TestServer; import org.openhab.core.test.java.JavaTest; @@ -61,10 +63,11 @@ public abstract class AbstractAudioServletTest extends JavaTest { public @Mock @NonNullByDefault({}) HttpService httpServiceMock; public @Mock @NonNullByDefault({}) HttpContext httpContextMock; + public AudioSinkUtils audioSinkUtils = new AudioSinkUtilsImpl(); @BeforeEach public void setupServerAndClient() { - audioServlet = new AudioServlet(); + audioServlet = new AudioServlet(audioSinkUtils); ServletHolder servletHolder = new ServletHolder(audioServlet); diff --git a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java index 30430242b04..6f7200604a6 100644 --- a/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java +++ b/bundles/org.openhab.core.audio/src/test/java/org/openhab/core/audio/internal/AudioServletTest.java @@ -30,7 +30,7 @@ import org.openhab.core.audio.ByteArrayAudioStream; import org.openhab.core.audio.FileAudioStream; import org.openhab.core.audio.FixedLengthAudioStream; -import org.openhab.core.audio.internal.AudioServlet.StreamServed; +import org.openhab.core.audio.StreamServed; import org.openhab.core.audio.internal.utils.BundledSoundFileHandler; /** diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java index 62f00d4d754..fbb20b9c40f 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java @@ -42,7 +42,6 @@ import org.openhab.core.audio.AudioStream; import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioStreamException; -import org.openhab.core.audio.utils.AudioSinkUtils; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.core.ConfigOptionProvider; import org.openhab.core.config.core.ConfigurableService; @@ -229,6 +228,7 @@ public void say(String text, @Nullable String voiceId, @Nullable String sinkId) public void say(String text, @Nullable String voiceId, @Nullable String sinkId, @Nullable PercentType volume) { Objects.requireNonNull(text, "Text cannot be said as it is null."); + Runnable volumeRestauration = null; try { TTSService tts = null; Voice voice = null; @@ -272,16 +272,17 @@ public void say(String text, @Nullable String voiceId, @Nullable String sinkId, throw new TTSException( "Failed playing audio stream '" + audioStream + "' as audio sink doesn't support it"); } - - Runnable volumeRestoration = AudioSinkUtils.handleVolumeCommand(volume, sink, logger); - - sink.process(audioStream, volumeRestoration); + volumeRestauration = audioManager.handleVolumeCommand(volume, sink); + sink.processAndComplete(audioStream).thenRun(volumeRestauration); } catch (TTSException | UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { if (logger.isDebugEnabled()) { logger.debug("Error saying '{}': {}", text, e.getMessage(), e); } else { logger.warn("Error saying '{}': {}", text, e.getMessage()); } + if (volumeRestauration != null) { + volumeRestauration.run(); + } } }