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;
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_START_STREAM;
19 import java.io.IOException;
20 import java.net.InetSocketAddress;
21 import java.nio.charset.StandardCharsets;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
26 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.thing.ChannelUID;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import io.netty.buffer.ByteBuf;
33 import io.netty.buffer.Unpooled;
34 import io.netty.channel.ChannelHandlerContext;
35 import io.netty.channel.ChannelInboundHandlerAdapter;
36 import io.netty.handler.codec.http.DefaultHttpResponse;
37 import io.netty.handler.codec.http.HttpHeaderNames;
38 import io.netty.handler.codec.http.HttpHeaderValues;
39 import io.netty.handler.codec.http.HttpMethod;
40 import io.netty.handler.codec.http.HttpRequest;
41 import io.netty.handler.codec.http.HttpResponse;
42 import io.netty.handler.codec.http.HttpResponseStatus;
43 import io.netty.handler.codec.http.HttpVersion;
44 import io.netty.handler.codec.http.QueryStringDecoder;
45 import io.netty.handler.stream.ChunkedFile;
46 import io.netty.handler.timeout.IdleState;
47 import io.netty.handler.timeout.IdleStateEvent;
48 import io.netty.util.ReferenceCountUtil;
51 * The {@link StreamServerGroupHandler} class is responsible for handling streams and sending any requested files to
53 * features for a group of cameras instead of individual cameras.
55 * @author Matthew Skinner - Initial contribution
59 public class StreamServerGroupHandler extends ChannelInboundHandlerAdapter {
60 private final Logger logger = LoggerFactory.getLogger(getClass());
61 private IpCameraGroupHandler ipCameraGroupHandler;
62 private String whiteList = "";
64 public StreamServerGroupHandler(IpCameraGroupHandler ipCameraGroupHandler) {
65 this.ipCameraGroupHandler = ipCameraGroupHandler;
66 whiteList = ipCameraGroupHandler.groupConfig.getIpWhitelist();
70 public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
73 private String resolveIndexToPath(String uri) {
74 if (!uri.substring(1, 2).equals("i")) {
75 return ipCameraGroupHandler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
78 // example is /1ipcameraxx.ts
82 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
83 if (msg == null || ctx == null) {
87 if (msg instanceof HttpRequest) {
88 HttpRequest httpRequest = (HttpRequest) msg;
89 String requestIP = "("
90 + ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
91 if (!whiteList.contains(requestIP) && !whiteList.equals("DISABLE")) {
92 logger.warn("The request made from {} was not in the whitelist and will be ignored.", requestIP);
94 } else if (HttpMethod.GET.equals(httpRequest.method())) {
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 (ipCameraGroupHandler.hlsTurnedOn) {
100 String debugMe = ipCameraGroupHandler.getPlayList();
101 logger.debug("playlist is:{}", debugMe);
102 sendString(ctx, debugMe, "application/x-mpegurl");
106 "HLS requires the groups startStream channel to be turned on first. Just starting it now.");
107 String channelPrefix = "ipcamera:" + ipCameraGroupHandler.getThing().getThingTypeUID()
108 + ":" + ipCameraGroupHandler.getThing().getUID().getId() + ":";
109 ipCameraGroupHandler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM),
113 case "/ipcamera.jpg":
114 sendSnapshotImage(ctx, "image/jpg");
117 if (httpRequest.uri().contains(".ts")) {
118 sendFile(ctx, resolveIndexToPath(httpRequest.uri()) + httpRequest.uri().substring(2),
120 } else if (httpRequest.uri().contains(".jpg")) {
121 sendFile(ctx, httpRequest.uri(), "image/jpg");
122 } else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
123 sendFile(ctx, httpRequest.uri(), "video/mp4");
129 ReferenceCountUtil.release(msg);
133 private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
134 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
135 if (ipCameraGroupHandler.cameraIndex >= ipCameraGroupHandler.cameraOrder.size()) {
136 logger.debug("WARN: Openhab may still be starting, or all cameras in the group are OFFLINE.");
139 IpCameraHandler handler = ipCameraGroupHandler.cameraOrder.get(ipCameraGroupHandler.cameraIndex);
140 handler.lockCurrentSnapshot.lock();
142 ByteBuf snapshotData = Unpooled.copiedBuffer(handler.currentSnapshot);
143 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
144 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
145 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
146 response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
147 response.headers().add("Access-Control-Allow-Origin", "*");
148 response.headers().add("Access-Control-Expose-Headers", "*");
149 ctx.channel().write(response);
150 ctx.channel().write(snapshotData);
151 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
152 ctx.channel().writeAndFlush(footerBbuf);
154 handler.lockCurrentSnapshot.unlock();
158 private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
159 logger.trace("file is :{}", fileUri);
160 File file = new File(fileUri);
161 ChunkedFile chunkedFile = new ChunkedFile(file);
162 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
163 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
164 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
165 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
166 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
167 response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
168 response.headers().add("Access-Control-Allow-Origin", "*");
169 response.headers().add("Access-Control-Expose-Headers", "*");
170 ctx.channel().write(response);
171 ctx.channel().write(chunkedFile);
172 ctx.channel().writeAndFlush(footerBbuf);
175 private void sendString(ChannelHandlerContext ctx, String contents, String contentType) {
176 ByteBuf contentsBbuf = Unpooled.copiedBuffer(contents, 0, contents.length(), StandardCharsets.UTF_8);
177 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
178 response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
179 response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
180 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
181 response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentsBbuf.readableBytes());
182 response.headers().add("Access-Control-Allow-Origin", "*");
183 response.headers().add("Access-Control-Expose-Headers", "*");
184 ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
185 ctx.channel().write(response);
186 ctx.channel().write(contentsBbuf);
187 ctx.channel().writeAndFlush(footerBbuf);
191 public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
195 public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
196 if (cause == null || ctx == null) {
199 if (cause.toString().contains("Connection reset by peer")) {
200 logger.debug("Connection reset by peer.");
201 } else if (cause.toString().contains("An established connection was aborted by the software")) {
202 logger.debug("An established connection was aborted by the software");
203 } else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
204 logger.debug("An existing connection was forcibly closed by the remote host");
205 } else if (cause.toString().contains("(No such file or directory)")) {
207 "IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
209 logger.warn("Exception caught from stream server:{}", cause.getMessage());
215 public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
216 if (evt == null || ctx == null) {
219 if (evt instanceof IdleStateEvent) {
220 IdleStateEvent e = (IdleStateEvent) evt;
221 if (e.state() == IdleState.WRITER_IDLE) {
222 logger.debug("Stream server is going to close an idle channel.");
229 public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {