]> git.basschouten.com Git - openhab-addons.git/blob
7629f23835fe1d194ee88aacd78dae7ce04ab258
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.sonos.internal;
14
15 import java.io.IOException;
16 import java.util.Collections;
17 import java.util.Locale;
18 import java.util.Set;
19 import java.util.stream.Collectors;
20 import java.util.stream.Stream;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.sonos.internal.handler.ZonePlayerHandler;
25 import org.openhab.core.audio.AudioFormat;
26 import org.openhab.core.audio.AudioHTTPServer;
27 import org.openhab.core.audio.AudioSink;
28 import org.openhab.core.audio.AudioStream;
29 import org.openhab.core.audio.FileAudioStream;
30 import org.openhab.core.audio.FixedLengthAudioStream;
31 import org.openhab.core.audio.URLAudioStream;
32 import org.openhab.core.audio.UnsupportedAudioFormatException;
33 import org.openhab.core.audio.UnsupportedAudioStreamException;
34 import org.openhab.core.audio.utils.AudioStreamUtils;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.util.ThingHandlerHelper;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * This makes a Sonos speaker to serve as an {@link AudioSink}-
44  *
45  * @author Kai Kreuzer - Initial contribution and API
46  * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
47  *
48  */
49 @NonNullByDefault
50 public class SonosAudioSink implements AudioSink {
51
52     private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.class);
53
54     private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Collections
55             .unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
56     private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Collections
57             .unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet()));
58
59     private AudioHTTPServer audioHTTPServer;
60     private ZonePlayerHandler handler;
61     private @Nullable String callbackUrl;
62
63     public SonosAudioSink(ZonePlayerHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
64         this.handler = handler;
65         this.audioHTTPServer = audioHTTPServer;
66         this.callbackUrl = callbackUrl;
67     }
68
69     @Override
70     public String getId() {
71         return handler.getThing().getUID().toString();
72     }
73
74     @Override
75     public @Nullable String getLabel(@Nullable Locale locale) {
76         return handler.getThing().getLabel();
77     }
78
79     @Override
80     public void process(@Nullable AudioStream audioStream)
81             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
82         if (audioStream == null) {
83             // in case the audioStream is null, this should be interpreted as a request to end any currently playing
84             // stream.
85             logger.trace("Stop currently playing stream.");
86             handler.stopPlaying(OnOffType.ON);
87         } else if (audioStream instanceof URLAudioStream) {
88             // it is an external URL, the speaker can access it itself and play it.
89             URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
90             handler.playURI(new StringType(urlAudioStream.getURL()));
91             try {
92                 audioStream.close();
93             } catch (IOException e) {
94             }
95         } else if (audioStream instanceof FixedLengthAudioStream) {
96             // we serve it on our own HTTP server and treat it as a notification
97             // Note that we have to pass a FixedLengthAudioStream, since Sonos does multiple concurrent requests to
98             // the AudioServlet, so a one time serving won't work.
99             if (callbackUrl != null) {
100                 String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10).toString();
101                 String url = callbackUrl + relativeUrl;
102
103                 AudioFormat format = audioStream.getFormat();
104                 if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
105                     logger.warn("Sonos speaker '{}' is not initialized - status is {}", handler.getThing().getUID(),
106                             handler.getThing().getStatus());
107                 } else if (AudioFormat.WAV.isCompatible(format)) {
108                     handler.playNotificationSoundURI(
109                             new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION));
110                 } else if (AudioFormat.MP3.isCompatible(format)) {
111                     handler.playNotificationSoundURI(
112                             new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION));
113                 } else {
114                     throw new UnsupportedAudioFormatException("Sonos only supports MP3 or WAV.", format);
115                 }
116             } else {
117                 logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!");
118             }
119         } else {
120             try {
121                 audioStream.close();
122             } catch (IOException e) {
123             }
124             throw new UnsupportedAudioStreamException(
125                     "Sonos can only handle FixedLengthAudioStreams and URLAudioStreams.", audioStream.getClass());
126             // Instead of throwing an exception, we could ourselves try to wrap it into a
127             // FixedLengthAudioStream, but this might be dangerous as we have no clue, how much data to expect from
128             // the stream.
129         }
130     }
131
132     @Override
133     public Set<AudioFormat> getSupportedFormats() {
134         return SUPPORTED_AUDIO_FORMATS;
135     }
136
137     @Override
138     public Set<Class<? extends AudioStream>> getSupportedStreams() {
139         return SUPPORTED_AUDIO_STREAMS;
140     }
141
142     @Override
143     public PercentType getVolume() {
144         String volume = handler.getVolume();
145         return volume != null ? new PercentType(volume) : PercentType.ZERO;
146     }
147
148     @Override
149     public void setVolume(PercentType volume) {
150         handler.setVolume(volume);
151     }
152 }