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