]> git.basschouten.com Git - openhab-addons.git/blob
deaed073e65ca416b93cbe835ba6b63c3e325e0f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.freeboxos.internal.handler;
14
15 import static org.openhab.core.audio.AudioFormat.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.HashSet;
20 import java.util.Locale;
21 import java.util.Set;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
26 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager;
27 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Action;
28 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.MediaType;
29 import org.openhab.core.audio.AudioFormat;
30 import org.openhab.core.audio.AudioHTTPServer;
31 import org.openhab.core.audio.AudioSinkAsync;
32 import org.openhab.core.audio.AudioStream;
33 import org.openhab.core.audio.StreamServed;
34 import org.openhab.core.audio.URLAudioStream;
35 import org.openhab.core.audio.UnsupportedAudioFormatException;
36 import org.openhab.core.audio.UnsupportedAudioStreamException;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.thing.ThingStatus;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * The {@link AirMediaSink} is holding AudioSink capabilities for various
44  * things.
45  *
46  * @author GaĆ«l L'hopital - Initial contribution
47  */
48 @NonNullByDefault
49 public class AirMediaSink extends AudioSinkAsync {
50     private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
51     // OGG seems to not be properly supported (tested with a file produced by VoiceRSS)
52     private static final Set<AudioFormat> BASIC_FORMATS = Set.of(WAV/* , OGG */);
53     private static final Set<AudioFormat> ALL_MP3_FORMATS = Set.of(
54             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 96000, null),
55             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 112000, null),
56             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 128000, null),
57             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 160000, null),
58             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 192000, null),
59             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 224000, null),
60             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 256000, null),
61             new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null));
62
63     private final Logger logger = LoggerFactory.getLogger(AirMediaSink.class);
64     private final ApiConsumerHandler thingHandler;
65     private final Set<AudioFormat> supportedFormats = new HashSet<>();
66     private final AudioHTTPServer audioHTTPServer;
67     private final String callbackUrl;
68     private final String playerName;
69     private final String password;
70
71     public AirMediaSink(ApiConsumerHandler thingHandler, AudioHTTPServer audioHTTPServer, String callbackUrl,
72             String playerName, String password, boolean acceptAllMp3) {
73         this.thingHandler = thingHandler;
74         this.audioHTTPServer = audioHTTPServer;
75         this.playerName = playerName;
76         this.callbackUrl = callbackUrl;
77         this.password = password;
78
79         supportedFormats.addAll(BASIC_FORMATS);
80         if (acceptAllMp3) {
81             supportedFormats.addAll(ALL_MP3_FORMATS);
82         } else { // Only accept MP3 bitrates >= 96 kbps
83             supportedFormats.add(MP3);
84         }
85     }
86
87     @Override
88     public Set<Class<? extends AudioStream>> getSupportedStreams() {
89         return SUPPORTED_STREAMS;
90     }
91
92     @Override
93     public PercentType getVolume() throws IOException {
94         logger.debug("getVolume received but AirMedia does not have the capability - returning 100%.");
95         return PercentType.HUNDRED;
96     }
97
98     @Override
99     public void setVolume(PercentType volume) throws IOException {
100         logger.debug("setVolume received but AirMedia does not have the capability - ignoring it.");
101     }
102
103     @Override
104     public String getId() {
105         return thingHandler.getThing().getUID().toString();
106     }
107
108     @Override
109     public @Nullable String getLabel(@Nullable Locale locale) {
110         return thingHandler.getThing().getLabel();
111     }
112
113     @Override
114     protected void processAsynchronously(@Nullable AudioStream audioStream)
115             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
116         if (thingHandler.getThing().getStatus() != ThingStatus.ONLINE) {
117             tryClose(audioStream);
118             return;
119         }
120
121         if (audioStream == null) {
122             stopMedia();
123             return;
124         }
125
126         String url;
127         if (audioStream instanceof URLAudioStream urlAudioStream) {
128             // it is an external URL, we can access it directly
129             url = urlAudioStream.getURL();
130             tryClose(audioStream);
131         } else {
132             // we serve it on our own HTTP server
133             logger.debug("audioStream {} {}", audioStream.getClass().getSimpleName(), audioStream.getFormat());
134             StreamServed streamServed;
135             try {
136                 streamServed = audioHTTPServer.serve(audioStream, 5, true);
137             } catch (IOException e) {
138                 tryClose(audioStream);
139                 throw new UnsupportedAudioStreamException(
140                         "AirPlay device was not able to handle the audio stream (cache on disk failed).",
141                         audioStream.getClass(), e);
142             }
143             url = callbackUrl + streamServed.url();
144             streamServed.playEnd().thenRun(() -> {
145                 stopMedia();
146                 this.playbackFinished(audioStream);
147             });
148         }
149         logger.debug("AirPlay audio sink: process url {}", url);
150         playMedia(url);
151     }
152
153     private void tryClose(@Nullable InputStream is) {
154         if (is != null) {
155             try {
156                 is.close();
157             } catch (IOException ignored) {
158             }
159         }
160     }
161
162     private void playMedia(String url) {
163         try {
164             MediaReceiverManager manager = thingHandler.getManager(MediaReceiverManager.class);
165             manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO);
166             manager.sendToReceiver(playerName, password, Action.START, MediaType.VIDEO, url);
167         } catch (FreeboxException e) {
168             logger.warn("Playing media failed: {}", e.getMessage());
169         }
170     }
171
172     private void stopMedia() {
173         try {
174             MediaReceiverManager manager = thingHandler.getManager(MediaReceiverManager.class);
175             manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO);
176         } catch (FreeboxException e) {
177             logger.warn("Stopping media failed: {}", e.getMessage());
178         }
179     }
180
181     @Override
182     public Set<AudioFormat> getSupportedFormats() {
183         return supportedFormats;
184     }
185 }