]> git.basschouten.com Git - openhab-addons.git/blob
6ec1c9b54178f6c436a88c787bbb3774454bae28
[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 java.io.File;
16 import java.io.IOException;
17 import java.net.InetSocketAddress;
18 import java.nio.charset.StandardCharsets;
19 import java.util.concurrent.TimeUnit;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
24 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import io.netty.buffer.ByteBuf;
29 import io.netty.buffer.Unpooled;
30 import io.netty.channel.ChannelHandlerContext;
31 import io.netty.channel.ChannelInboundHandlerAdapter;
32 import io.netty.handler.codec.http.DefaultHttpResponse;
33 import io.netty.handler.codec.http.HttpContent;
34 import io.netty.handler.codec.http.HttpHeaderNames;
35 import io.netty.handler.codec.http.HttpHeaderValues;
36 import io.netty.handler.codec.http.HttpRequest;
37 import io.netty.handler.codec.http.HttpResponse;
38 import io.netty.handler.codec.http.HttpResponseStatus;
39 import io.netty.handler.codec.http.HttpVersion;
40 import io.netty.handler.codec.http.LastHttpContent;
41 import io.netty.handler.codec.http.QueryStringDecoder;
42 import io.netty.handler.stream.ChunkedFile;
43 import io.netty.handler.timeout.IdleState;
44 import io.netty.handler.timeout.IdleStateEvent;
45 import io.netty.util.ReferenceCountUtil;
46
47 /**
48  * The {@link StreamServerHandler} class is responsible for handling streams and sending any requested files to openHABs
49  * features.
50  *
51  * @author Matthew Skinner - Initial contribution
52  */
53
54 @NonNullByDefault
55 public class StreamServerHandler extends ChannelInboundHandlerAdapter {
56     private final Logger logger = LoggerFactory.getLogger(getClass());
57     private IpCameraHandler ipCameraHandler;
58     private boolean handlingMjpeg = false; // used to remove ctx from group when handler is removed.
59     private boolean handlingSnapshotStream = false; // used to remove ctx from group when handler is removed.
60     private byte[] incomingJpeg = new byte[0];
61     private String whiteList = "";
62     private int recievedBytes = 0;
63     private boolean updateSnapshot = false;
64     private boolean onvifEvent = false;
65
66     public StreamServerHandler(IpCameraHandler ipCameraHandler) {
67         this.ipCameraHandler = ipCameraHandler;
68         whiteList = ipCameraHandler.getWhiteList();
69     }
70
71     @Override
72     public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
73     }
74
75     @Override
76     public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
77         if (ctx == null) {
78             return;
79         }
80
81         try {
82             if (msg instanceof HttpRequest) {
83                 HttpRequest httpRequest = (HttpRequest) msg;
84                 if (!whiteList.equals("DISABLE")) {
85                     String requestIP = "("
86                             + ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
87                     if (!whiteList.contains(requestIP)) {
88                         logger.warn("The request made from {} was not in the whitelist and will be ignored.",
89                                 requestIP);
90                         return;
91                     }
92                 }
93                 if ("GET".equalsIgnoreCase(httpRequest.method().toString())) {
94                     logger.debug("Stream Server recieved request \tGET:{}", httpRequest.uri());
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                             Ffmpeg localFfmpeg = ipCameraHandler.ffmpegHLS;
100                             if (localFfmpeg == null) {
101                                 ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
102                             } else if (!localFfmpeg.getIsAlive()) {
103                                 localFfmpeg.startConverting();
104                             } else {
105                                 localFfmpeg.setKeepAlive(8);
106                                 sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
107                                 return;
108                             }
109                             // Allow files to be created, or you get old m3u8 from the last time this ran.
110                             TimeUnit.MILLISECONDS.sleep(4500);
111                             sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
112                             return;
113                         case "/ipcamera.mpd":
114                             sendFile(ctx, httpRequest.uri(), "application/dash+xml");
115                             return;
116                         case "/ipcamera.gif":
117                             sendFile(ctx, httpRequest.uri(), "image/gif");
118                             return;
119                         case "/ipcamera.jpg":
120                             if (!ipCameraHandler.snapshotPolling && ipCameraHandler.snapshotUri != "") {
121                                 ipCameraHandler.sendHttpGET(ipCameraHandler.snapshotUri);
122                             }
123                             if (ipCameraHandler.currentSnapshot.length == 1) {
124                                 logger.warn("ipcamera.jpg was requested but there is no jpg in ram to send.");
125                                 return;
126                             }
127                             sendSnapshotImage(ctx, "image/jpg");
128                             return;
129                         case "/snapshots.mjpeg":
130                             handlingSnapshotStream = true;
131                             ipCameraHandler.startSnapshotPolling();
132                             ipCameraHandler.setupSnapshotStreaming(true, ctx, false);
133                             return;
134                         case "/ipcamera.mjpeg":
135                             ipCameraHandler.setupMjpegStreaming(true, ctx);
136                             handlingMjpeg = true;
137                             return;
138                         case "/autofps.mjpeg":
139                             handlingSnapshotStream = true;
140                             ipCameraHandler.setupSnapshotStreaming(true, ctx, true);
141                             return;
142                         case "/instar":
143                             InstarHandler instar = new InstarHandler(ipCameraHandler);
144                             instar.alarmTriggered(httpRequest.uri().toString());
145                             ctx.close();
146                             return;
147                         case "/ipcamera0.ts":
148                         default:
149                             if (httpRequest.uri().contains(".ts")) {
150                                 sendFile(ctx, queryStringDecoder.path(), "video/MP2T");
151                             } else if (httpRequest.uri().contains(".gif")) {
152                                 sendFile(ctx, queryStringDecoder.path(), "image/gif");
153                             } else if (httpRequest.uri().contains(".jpg")) {
154                                 // Allow access to the preroll and postroll jpg files
155                                 sendFile(ctx, queryStringDecoder.path(), "image/jpg");
156                             } else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
157                                 sendFile(ctx, queryStringDecoder.path(), "video/mp4");
158                             }
159                             return;
160                     }
161                 } else if ("POST".equalsIgnoreCase(httpRequest.method().toString())) {
162                     switch (httpRequest.uri()) {
163                         case "/ipcamera.jpg":
164                             break;
165                         case "/snapshot.jpg":
166                             updateSnapshot = true;
167                             break;
168                         case "/OnvifEvent":
169                             onvifEvent = true;
170                             break;
171                         default:
172                             logger.debug("Stream Server recieved unknown request \tPOST:{}", httpRequest.uri());
173                             break;
174                     }
175                 }
176             }
177             if (msg instanceof HttpContent) {
178                 HttpContent content = (HttpContent) msg;
179                 if (recievedBytes == 0) {
180                     incomingJpeg = new byte[content.content().readableBytes()];
181                     content.content().getBytes(0, incomingJpeg, 0, content.content().readableBytes());
182                 } else {
183                     byte[] temp = incomingJpeg;
184                     incomingJpeg = new byte[recievedBytes + content.content().readableBytes()];
185                     System.arraycopy(temp, 0, incomingJpeg, 0, temp.length);
186                     content.content().getBytes(0, incomingJpeg, temp.length, content.content().readableBytes());
187                 }
188                 recievedBytes = incomingJpeg.length;
189                 if (content instanceof LastHttpContent) {
190                     if (updateSnapshot) {
191                         ipCameraHandler.processSnapshot(incomingJpeg);
192                     } else if (onvifEvent) {
193                         ipCameraHandler.onvifCamera.eventRecieved(new String(incomingJpeg, StandardCharsets.UTF_8));
194                     } else { // handles the snapshots that make up mjpeg from rtsp to ffmpeg conversions.
195                         if (recievedBytes > 1000) {
196                             ipCameraHandler.sendMjpegFrame(incomingJpeg, ipCameraHandler.mjpegChannelGroup);
197                         }
198                     }
199                     recievedBytes = 0;
200                 }
201             }
202         } finally {
203             ReferenceCountUtil.release(msg);
204         }
205     }
206
207     private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
208         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
209         ipCameraHandler.lockCurrentSnapshot.lock();
210         try {
211             ByteBuf snapshotData = Unpooled.copiedBuffer(ipCameraHandler.currentSnapshot);
212             response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
213             response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
214             response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
215             response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
216             response.headers().add("Access-Control-Allow-Origin", "*");
217             response.headers().add("Access-Control-Expose-Headers", "*");
218             ctx.channel().write(response);
219             ctx.channel().write(snapshotData);
220             ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
221             ctx.channel().writeAndFlush(footerBbuf);
222         } finally {
223             ipCameraHandler.lockCurrentSnapshot.unlock();
224         }
225     }
226
227     private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
228         File file = new File(ipCameraHandler.cameraConfig.getFfmpegOutput() + fileUri);
229         ChunkedFile chunkedFile = new ChunkedFile(file);
230         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
231         response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
232         response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
233         response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
234         response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
235         response.headers().add("Access-Control-Allow-Origin", "*");
236         response.headers().add("Access-Control-Expose-Headers", "*");
237         ctx.channel().write(response);
238         ctx.channel().write(chunkedFile);
239         ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
240         ctx.channel().writeAndFlush(footerBbuf);
241     }
242
243     @Override
244     public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
245     }
246
247     @Override
248     public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
249         if (ctx == null || cause == null) {
250             return;
251         }
252         if (cause.toString().contains("Connection reset by peer")) {
253             logger.trace("Connection reset by peer.");
254         } else if (cause.toString().contains("An established connection was aborted by the software")) {
255             logger.debug("An established connection was aborted by the software");
256         } else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
257             logger.debug("An existing connection was forcibly closed by the remote host");
258         } else if (cause.toString().contains("(No such file or directory)")) {
259             logger.info(
260                     "IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
261         } else {
262             logger.warn("Exception caught from stream server:{}", cause.getMessage());
263         }
264         ctx.close();
265     }
266
267     @Override
268     public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
269         if (ctx == null) {
270             return;
271         }
272         if (evt instanceof IdleStateEvent) {
273             IdleStateEvent e = (IdleStateEvent) evt;
274             if (e.state() == IdleState.WRITER_IDLE) {
275                 logger.debug("Stream server is going to close an idle channel.");
276                 ctx.close();
277             }
278         }
279     }
280
281     @Override
282     public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
283         if (ctx == null) {
284             return;
285         }
286         ctx.close();
287         if (handlingMjpeg) {
288             ipCameraHandler.setupMjpegStreaming(false, ctx);
289         } else if (handlingSnapshotStream) {
290             handlingSnapshotStream = false;
291             ipCameraHandler.setupSnapshotStreaming(false, ctx, false);
292         }
293     }
294 }