2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
14 package org.openhab.binding.ipcamera.internal;
17 import java.io.IOException;
18 import java.net.InetSocketAddress;
19 import java.nio.charset.StandardCharsets;
20 import java.util.concurrent.TimeUnit;
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;
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;
49 * The {@link StreamServerHandler} class is responsible for handling streams and sending any requested files to openHABs
52 * @author Matthew Skinner - Initial contribution
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;
67 public StreamServerHandler(IpCameraHandler ipCameraHandler) {
68 this.ipCameraHandler = ipCameraHandler;
69 whiteList = ipCameraHandler.getWhiteList();
73 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
77 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
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.",
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();
106 localFfmpeg.setKeepAlive(8);
107 sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
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");
114 case "/ipcamera.mpd":
115 sendFile(ctx, httpRequest.uri(), "application/dash+xml");
117 case "/ipcamera.gif":
118 sendFile(ctx, httpRequest.uri(), "image/gif");
120 case "/ipcamera.jpg":
121 if (!ipCameraHandler.snapshotPolling && ipCameraHandler.snapshotUri != "") {
122 ipCameraHandler.sendHttpGET(ipCameraHandler.snapshotUri);
124 if (ipCameraHandler.currentSnapshot.length == 1) {
125 logger.warn("ipcamera.jpg was requested but there is no jpg in ram to send.");
128 sendSnapshotImage(ctx, "image/jpg");
130 case "/snapshots.mjpeg":
131 handlingSnapshotStream = true;
132 ipCameraHandler.startSnapshotPolling();
133 ipCameraHandler.setupSnapshotStreaming(true, ctx, false);
135 case "/ipcamera.mjpeg":
136 ipCameraHandler.setupMjpegStreaming(true, ctx);
137 handlingMjpeg = true;
139 case "/autofps.mjpeg":
140 handlingSnapshotStream = true;
141 ipCameraHandler.setupSnapshotStreaming(true, ctx, true);
144 InstarHandler instar = new InstarHandler(ipCameraHandler);
145 instar.alarmTriggered(httpRequest.uri().toString());
148 case "/ipcamera0.ts":
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");
162 } else if ("POST".equalsIgnoreCase(httpRequest.method().toString())) {
163 switch (httpRequest.uri()) {
164 case "/ipcamera.jpg":
166 case "/snapshot.jpg":
167 updateSnapshot = true;
173 logger.debug("Stream Server recieved unknown request \tPOST:{}", httpRequest.uri());
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());
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());
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);
204 ReferenceCountUtil.release(msg);
208 private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
209 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
210 ipCameraHandler.lockCurrentSnapshot.lock();
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);
224 ipCameraHandler.lockCurrentSnapshot.unlock();
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);
245 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
249 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
250 if (ctx == null || cause == null) {
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)")) {
261 "IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
263 logger.warn("Exception caught from stream server:{}", cause.getMessage());
269 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
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.");
283 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
289 ipCameraHandler.setupMjpegStreaming(false, ctx);
290 } else if (handlingSnapshotStream) {
291 handlingSnapshotStream = false;
292 ipCameraHandler.setupSnapshotStreaming(false, ctx, false);