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