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