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