]> git.basschouten.com Git - openhab-addons.git/blob
db4cb22895e4f6daf2438ccb4ed7d7d8cfec70be
[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
14 package org.openhab.binding.ipcamera.internal;
15
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_START_STREAM;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.net.InetSocketAddress;
21 import java.nio.charset.StandardCharsets;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
26 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.thing.ChannelUID;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 import io.netty.buffer.ByteBuf;
33 import io.netty.buffer.Unpooled;
34 import io.netty.channel.ChannelHandlerContext;
35 import io.netty.channel.ChannelInboundHandlerAdapter;
36 import io.netty.handler.codec.http.DefaultHttpResponse;
37 import io.netty.handler.codec.http.HttpHeaderNames;
38 import io.netty.handler.codec.http.HttpHeaderValues;
39 import io.netty.handler.codec.http.HttpMethod;
40 import io.netty.handler.codec.http.HttpRequest;
41 import io.netty.handler.codec.http.HttpResponse;
42 import io.netty.handler.codec.http.HttpResponseStatus;
43 import io.netty.handler.codec.http.HttpVersion;
44 import io.netty.handler.codec.http.QueryStringDecoder;
45 import io.netty.handler.stream.ChunkedFile;
46 import io.netty.handler.timeout.IdleState;
47 import io.netty.handler.timeout.IdleStateEvent;
48 import io.netty.util.ReferenceCountUtil;
49
50 /**
51  * The {@link StreamServerGroupHandler} class is responsible for handling streams and sending any requested files to
52  * Openhabs
53  * features for a group of cameras instead of individual cameras.
54  *
55  * @author Matthew Skinner - Initial contribution
56  */
57
58 @NonNullByDefault
59 public class StreamServerGroupHandler extends ChannelInboundHandlerAdapter {
60     private final Logger logger = LoggerFactory.getLogger(getClass());
61     private IpCameraGroupHandler ipCameraGroupHandler;
62     private String whiteList = "";
63
64     public StreamServerGroupHandler(IpCameraGroupHandler ipCameraGroupHandler) {
65         this.ipCameraGroupHandler = ipCameraGroupHandler;
66         whiteList = ipCameraGroupHandler.groupConfig.getIpWhitelist();
67     }
68
69     @Override
70     public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
71     }
72
73     private String resolveIndexToPath(String uri) {
74         if (!uri.substring(1, 2).equals("i")) {
75             return ipCameraGroupHandler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
76         }
77         return "notFound";
78         // example is /1ipcameraxx.ts
79     }
80
81     @Override
82     public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
83         if (msg == null || ctx == null) {
84             return;
85         }
86         try {
87             if (msg instanceof HttpRequest) {
88                 HttpRequest httpRequest = (HttpRequest) msg;
89                 String requestIP = "("
90                         + ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
91                 if (!whiteList.contains(requestIP) && !whiteList.equals("DISABLE")) {
92                     logger.warn("The request made from {} was not in the whitelist and will be ignored.", requestIP);
93                     return;
94                 } else if (HttpMethod.GET.equals(httpRequest.method())) {
95                     // Some browsers send a query string after the path when refreshing a picture.
96                     QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
97                     switch (queryStringDecoder.path()) {
98                         case "/ipcamera.m3u8":
99                             if (ipCameraGroupHandler.hlsTurnedOn) {
100                                 String debugMe = ipCameraGroupHandler.getPlayList();
101                                 logger.debug("playlist is:{}", debugMe);
102                                 sendString(ctx, debugMe, "application/x-mpegurl");
103                                 return;
104                             } else {
105                                 logger.warn(
106                                         "HLS requires the groups startStream channel to be turned on first. Just starting it now.");
107                                 String channelPrefix = "ipcamera:" + ipCameraGroupHandler.getThing().getThingTypeUID()
108                                         + ":" + ipCameraGroupHandler.getThing().getUID().getId() + ":";
109                                 ipCameraGroupHandler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM),
110                                         OnOffType.ON);
111                             }
112                             break;
113                         case "/ipcamera.jpg":
114                             sendSnapshotImage(ctx, "image/jpg");
115                             return;
116                         default:
117                             if (httpRequest.uri().contains(".ts")) {
118                                 sendFile(ctx, resolveIndexToPath(httpRequest.uri()) + httpRequest.uri().substring(2),
119                                         "video/MP2T");
120                             } else if (httpRequest.uri().contains(".jpg")) {
121                                 sendFile(ctx, httpRequest.uri(), "image/jpg");
122                             } else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
123                                 sendFile(ctx, httpRequest.uri(), "video/mp4");
124                             }
125                     }
126                 }
127             }
128         } finally {
129             ReferenceCountUtil.release(msg);
130         }
131     }
132
133     private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
134         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
135         if (ipCameraGroupHandler.cameraIndex >= ipCameraGroupHandler.cameraOrder.size()) {
136             logger.debug("WARN: Openhab may still be starting, or all cameras in the group are OFFLINE.");
137             return;
138         }
139         IpCameraHandler handler = ipCameraGroupHandler.cameraOrder.get(ipCameraGroupHandler.cameraIndex);
140         handler.lockCurrentSnapshot.lock();
141         try {
142             ByteBuf snapshotData = Unpooled.copiedBuffer(handler.currentSnapshot);
143             response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
144             response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
145             response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
146             response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
147             response.headers().add("Access-Control-Allow-Origin", "*");
148             response.headers().add("Access-Control-Expose-Headers", "*");
149             ctx.channel().write(response);
150             ctx.channel().write(snapshotData);
151             ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
152             ctx.channel().writeAndFlush(footerBbuf);
153         } finally {
154             handler.lockCurrentSnapshot.unlock();
155         }
156     }
157
158     private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
159         logger.trace("file is :{}", fileUri);
160         File file = new File(fileUri);
161         ChunkedFile chunkedFile = new ChunkedFile(file);
162         ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
163         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
164         response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
165         response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
166         response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
167         response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
168         response.headers().add("Access-Control-Allow-Origin", "*");
169         response.headers().add("Access-Control-Expose-Headers", "*");
170         ctx.channel().write(response);
171         ctx.channel().write(chunkedFile);
172         ctx.channel().writeAndFlush(footerBbuf);
173     }
174
175     private void sendString(ChannelHandlerContext ctx, String contents, String contentType) {
176         ByteBuf contentsBbuf = Unpooled.copiedBuffer(contents, 0, contents.length(), StandardCharsets.UTF_8);
177         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
178         response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
179         response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
180         response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
181         response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentsBbuf.readableBytes());
182         response.headers().add("Access-Control-Allow-Origin", "*");
183         response.headers().add("Access-Control-Expose-Headers", "*");
184         ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
185         ctx.channel().write(response);
186         ctx.channel().write(contentsBbuf);
187         ctx.channel().writeAndFlush(footerBbuf);
188     }
189
190     @Override
191     public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
192     }
193
194     @Override
195     public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
196         if (cause == null || ctx == null) {
197             return;
198         }
199         if (cause.toString().contains("Connection reset by peer")) {
200             logger.debug("Connection reset by peer.");
201         } else if (cause.toString().contains("An established connection was aborted by the software")) {
202             logger.debug("An established connection was aborted by the software");
203         } else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
204             logger.debug("An existing connection was forcibly closed by the remote host");
205         } else if (cause.toString().contains("(No such file or directory)")) {
206             logger.info(
207                     "IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
208         } else {
209             logger.warn("Exception caught from stream server:{}", cause.getMessage());
210         }
211         ctx.close();
212     }
213
214     @Override
215     public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
216         if (evt == null || ctx == null) {
217             return;
218         }
219         if (evt instanceof IdleStateEvent) {
220             IdleStateEvent e = (IdleStateEvent) evt;
221             if (e.state() == IdleState.WRITER_IDLE) {
222                 logger.debug("Stream server is going to close an idle channel.");
223                 ctx.close();
224             }
225         }
226     }
227
228     @Override
229     public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
230         if (ctx == null) {
231             return;
232         }
233         ctx.close();
234     }
235 }