]> git.basschouten.com Git - openhab-addons.git/blob
f578b76a2f967ce31fb98b3f29cbed698d86deaf
[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
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                             if (ipCameraHandler.ffmpegHLS != null) {
100                                 if (!ipCameraHandler.ffmpegHLS.getIsAlive()) {
101                                     if (ipCameraHandler.ffmpegHLS != null) {
102                                         ipCameraHandler.ffmpegHLS.startConverting();
103                                     }
104                                 }
105                             } else {
106                                 ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
107                             }
108                             if (ipCameraHandler.ffmpegHLS != null) {
109                                 ipCameraHandler.ffmpegHLS.setKeepAlive(8);
110                             }
111                             sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
112                             ctx.close();
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                 int index = 0;
181                 if (recievedBytes == 0) {
182                     incomingJpeg = new byte[content.content().capacity()];
183                 } else {
184                     byte[] temp = incomingJpeg;
185                     incomingJpeg = new byte[recievedBytes + content.content().capacity()];
186
187                     for (; index < temp.length; index++) {
188                         incomingJpeg[index] = temp[index];
189                     }
190                 }
191                 for (int i = 0; i < content.content().capacity(); i++) {
192                     incomingJpeg[index++] = content.content().getByte(i);
193                 }
194                 recievedBytes = incomingJpeg.length;
195                 if (content instanceof LastHttpContent) {
196                     if (updateSnapshot) {
197                         ipCameraHandler.processSnapshot(incomingJpeg);
198                     } else if (onvifEvent) {
199                         ipCameraHandler.onvifCamera.eventRecieved(new String(incomingJpeg, StandardCharsets.UTF_8));
200                     } else { // handles the snapshots that make up mjpeg from rtsp to ffmpeg conversions.
201                         if (recievedBytes > 1000) {
202                             ipCameraHandler.sendMjpegFrame(incomingJpeg, ipCameraHandler.mjpegChannelGroup);
203                         }
204                     }
205                     recievedBytes = 0;
206                 }
207             }
208         } finally {
209             ReferenceCountUtil.release(msg);
210         }
211     }
212
213     private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
214         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
215         ipCameraHandler.lockCurrentSnapshot.lock();
216         try {
217             ByteBuf snapshotData = Unpooled.copiedBuffer(ipCameraHandler.currentSnapshot);
218             response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
219             response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
220             response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
221             response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
222             response.headers().add("Access-Control-Allow-Origin", "*");
223             response.headers().add("Access-Control-Expose-Headers", "*");
224             ctx.channel().write(response);
225             ctx.channel().write(snapshotData);
226             ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
227             ctx.channel().writeAndFlush(footerBbuf);
228         } finally {
229             ipCameraHandler.lockCurrentSnapshot.unlock();
230         }
231     }
232
233     private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
234         File file = new File(ipCameraHandler.cameraConfig.getFfmpegOutput() + fileUri);
235         ChunkedFile chunkedFile = new ChunkedFile(file);
236         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
237         response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
238         response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
239         response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
240         response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
241         response.headers().add("Access-Control-Allow-Origin", "*");
242         response.headers().add("Access-Control-Expose-Headers", "*");
243         ctx.channel().write(response);
244         ctx.channel().write(chunkedFile);
245         ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
246         ctx.channel().writeAndFlush(footerBbuf);
247     }
248
249     @Override
250     public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
251     }
252
253     @Override
254     public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
255         if (ctx == null || cause == null) {
256             return;
257         }
258         if (cause.toString().contains("Connection reset by peer")) {
259             logger.trace("Connection reset by peer.");
260         } else if (cause.toString().contains("An established connection was aborted by the software")) {
261             logger.debug("An established connection was aborted by the software");
262         } else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
263             logger.debug("An existing connection was forcibly closed by the remote host");
264         } else if (cause.toString().contains("(No such file or directory)")) {
265             logger.info(
266                     "IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
267         } else {
268             logger.warn("Exception caught from stream server:{}", cause.getMessage());
269         }
270         ctx.close();
271     }
272
273     @Override
274     public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
275         if (ctx == null) {
276             return;
277         }
278         if (evt instanceof IdleStateEvent) {
279             IdleStateEvent e = (IdleStateEvent) evt;
280             if (e.state() == IdleState.WRITER_IDLE) {
281                 logger.debug("Stream server is going to close an idle channel.");
282                 ctx.close();
283             }
284         }
285     }
286
287     @Override
288     public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
289         if (ctx == null) {
290             return;
291         }
292         ctx.close();
293         if (handlingMjpeg) {
294             ipCameraHandler.setupMjpegStreaming(false, ctx);
295         } else if (handlingSnapshotStream) {
296             handlingSnapshotStream = false;
297             ipCameraHandler.setupSnapshotStreaming(false, ctx, false);
298         }
299     }
300 }