[ipcamera] Move to using port 8080 servlet not Netty. (#11160)
* Move to using 8080 servlet not Netty. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Add some mjpeg features to servlet. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Fix autofps bug Signed-off-by: Matthew Skinner <matt@pcmus.com> * Reached feature parity. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Cleanup serverPort from cameras. Signed-off-by: Matthew Skinner <matt@pcmus.com> * bug fixes. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Refactor groups. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Bug fixes to groups Signed-off-by: Matthew Skinner <matt@pcmus.com> * Update readme Signed-off-by: Matthew Skinner <matt@pcmus.com> * Cleanup Signed-off-by: Matthew Skinner <matt@pcmus.com> * clean up 2. Signed-off-by: Matthew Skinner <matt@pcmus.com> * bug fixes. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Improve snapshot fetching for autofps. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Make functions synchronized. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Fixes. Signed-off-by: Matthew Skinner <matt@pcmus.com> * Abstract servlets Signed-off-by: Matthew Skinner <matt@pcmus.com> * Fix NPE warnings Signed-off-by: Matthew Skinner <matt@pcmus.com> * Remove ability to go child or parent folders. Signed-off-by: Matthew Skinner <matt@pcmus.com> * autofps improvement Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
@@ -25,7 +25,6 @@ public class CameraConfig {
|
||||
private String ffmpegInputOptions = "";
|
||||
private int port;
|
||||
private int onvifPort;
|
||||
private int serverPort;
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private int onvifMediaProfile;
|
||||
@@ -142,10 +141,6 @@ public class CameraConfig {
|
||||
return onvifPort;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GroupConfig {
|
||||
private int pollTime, serverPort;
|
||||
private int pollTime;
|
||||
private boolean motionChangesOrder = true;
|
||||
private String ipWhitelist = "";
|
||||
private String ffmpegLocation = "";
|
||||
@@ -63,10 +63,6 @@ public class GroupConfig {
|
||||
return ffmpegOutput;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public int getPollTime() {
|
||||
return pollTime;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
@@ -27,7 +28,7 @@ import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
|
||||
@NonNullByDefault
|
||||
public class GroupTracker {
|
||||
public ArrayList<IpCameraHandler> listOfOnlineCameraHandlers = new ArrayList<>(1);
|
||||
public ArrayList<IpCameraGroupHandler> listOfGroupHandlers = new ArrayList<>(0);
|
||||
public ArrayList<String> listOfOnlineCameraUID = new ArrayList<>(1);
|
||||
public Set<IpCameraHandler> listOfOnlineCameraHandlers = new CopyOnWriteArraySet<>();
|
||||
public Set<IpCameraGroupHandler> listOfGroupHandlers = new CopyOnWriteArraySet<>();
|
||||
public Set<String> listOfOnlineCameraUID = new CopyOnWriteArraySet<>();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
@@ -43,8 +42,8 @@ public class InstarHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String requestUrl = "Empty";
|
||||
|
||||
public InstarHandler(ThingHandler thingHandler) {
|
||||
ipCameraHandler = (IpCameraHandler) thingHandler;
|
||||
public InstarHandler(IpCameraHandler thingHandler) {
|
||||
ipCameraHandler = thingHandler;
|
||||
}
|
||||
|
||||
public void setURL(String url) {
|
||||
@@ -185,7 +184,7 @@ public class InstarHandler extends ChannelDuplexHandler {
|
||||
}
|
||||
}
|
||||
|
||||
void alarmTriggered(String alarm) {
|
||||
public void alarmTriggered(String alarm) {
|
||||
ipCameraHandler.logger.debug("Alarm has been triggered:{}", alarm);
|
||||
switch (alarm) {
|
||||
case "/instar?&active=1":// The motion area boxes 1-4
|
||||
|
||||
@@ -44,6 +44,9 @@ public class IpCameraBindingConstants {
|
||||
}
|
||||
|
||||
public static final BigDecimal BIG_DECIMAL_SCALE_MOTION = new BigDecimal(5000);
|
||||
public static final long HLS_STARTUP_DELAY_MS = 4500;
|
||||
@SuppressWarnings("null")
|
||||
public static final int SERVLET_PORT = Integer.getInteger("org.osgi.service.http.port", 8080);
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraHandlerFactory} is responsible for creating things and thing
|
||||
@@ -40,12 +41,15 @@ public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final @Nullable String openhabIpAddress;
|
||||
private final GroupTracker groupTracker = new GroupTracker();
|
||||
private final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
|
||||
private final HttpService httpService;
|
||||
|
||||
@Activate
|
||||
public IpCameraHandlerFactory(final @Reference NetworkAddressService networkAddressService,
|
||||
final @Reference IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
|
||||
final @Reference IpCameraDynamicStateDescriptionProvider stateDescriptionProvider,
|
||||
final @Reference HttpService httpService) {
|
||||
openhabIpAddress = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.httpService = httpService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,9 +62,9 @@ public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraHandler(thing, openhabIpAddress, groupTracker, stateDescriptionProvider);
|
||||
return new IpCameraHandler(thing, openhabIpAddress, groupTracker, stateDescriptionProvider, httpService);
|
||||
} else if (GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraGroupHandler(thing, openhabIpAddress, groupTracker);
|
||||
return new IpCameraGroupHandler(thing, openhabIpAddress, groupTracker, httpService);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_START_STREAM;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerGroupHandler} class is responsible for handling streams and sending any requested files to
|
||||
* Openhabs
|
||||
* features for a group of cameras instead of individual cameras.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerGroupHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraGroupHandler ipCameraGroupHandler;
|
||||
private String whiteList = "";
|
||||
|
||||
public StreamServerGroupHandler(IpCameraGroupHandler ipCameraGroupHandler) {
|
||||
this.ipCameraGroupHandler = ipCameraGroupHandler;
|
||||
whiteList = ipCameraGroupHandler.groupConfig.getIpWhitelist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
private String resolveIndexToPath(String uri) {
|
||||
if (!"i".equals(uri.substring(1, 2))) {
|
||||
return ipCameraGroupHandler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
|
||||
}
|
||||
return "notFound";
|
||||
// example is /1ipcameraxx.ts
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP) && !"DISABLE".equals(whiteList)) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.", requestIP);
|
||||
return;
|
||||
} else if (HttpMethod.GET.equals(httpRequest.method())) {
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (ipCameraGroupHandler.hlsTurnedOn) {
|
||||
String debugMe = ipCameraGroupHandler.getPlayList();
|
||||
logger.debug("playlist is:{}", debugMe);
|
||||
sendString(ctx, debugMe, "application/x-mpegurl");
|
||||
return;
|
||||
} else {
|
||||
logger.warn(
|
||||
"HLS requires the groups startStream channel to be turned on first. Just starting it now.");
|
||||
String channelPrefix = "ipcamera:" + ipCameraGroupHandler.getThing().getThingTypeUID()
|
||||
+ ":" + ipCameraGroupHandler.getThing().getUID().getId() + ":";
|
||||
ipCameraGroupHandler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM),
|
||||
OnOffType.ON);
|
||||
}
|
||||
break;
|
||||
case "/ipcamera.jpg":
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, resolveIndexToPath(httpRequest.uri()) + httpRequest.uri().substring(2),
|
||||
"video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
sendFile(ctx, httpRequest.uri(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, httpRequest.uri(), "video/mp4");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
if (ipCameraGroupHandler.cameraIndex >= ipCameraGroupHandler.cameraOrder.size()) {
|
||||
logger.debug("WARN: Openhab may still be starting, or all cameras in the group are OFFLINE.");
|
||||
return;
|
||||
}
|
||||
IpCameraHandler handler = ipCameraGroupHandler.cameraOrder.get(ipCameraGroupHandler.cameraIndex);
|
||||
handler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(handler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
handler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
logger.trace("file is :{}", fileUri);
|
||||
File file = new File(fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
private void sendString(ChannelHandlerContext ctx, String contents, String contentType) {
|
||||
ByteBuf contentsBbuf = Unpooled.copiedBuffer(contents, 0, contents.length(), StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentsBbuf.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(contentsBbuf);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (cause == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.debug("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (evt == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerHandler} class is responsible for handling streams and sending any requested files to openHABs
|
||||
* features.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private boolean handlingMjpeg = false; // used to remove ctx from group when handler is removed.
|
||||
private boolean handlingSnapshotStream = false; // used to remove ctx from group when handler is removed.
|
||||
private byte[] incomingJpeg = new byte[0];
|
||||
private String whiteList = "";
|
||||
private int recievedBytes = 0;
|
||||
private boolean updateSnapshot = false;
|
||||
private boolean onvifEvent = false;
|
||||
|
||||
public StreamServerHandler(IpCameraHandler ipCameraHandler) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
whiteList = ipCameraHandler.getWhiteList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
if (!"DISABLE".equals(whiteList)) {
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP)) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.",
|
||||
requestIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ("GET".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
logger.debug("Stream Server recieved request \tGET:{}", httpRequest.uri());
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
Ffmpeg localFfmpeg = ipCameraHandler.ffmpegHLS;
|
||||
if (localFfmpeg == null) {
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
|
||||
} else if (!localFfmpeg.getIsAlive()) {
|
||||
localFfmpeg.startConverting();
|
||||
} else {
|
||||
localFfmpeg.setKeepAlive(8);
|
||||
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
|
||||
return;
|
||||
}
|
||||
// Allow files to be created, or you get old m3u8 from the last time this ran.
|
||||
TimeUnit.MILLISECONDS.sleep(4500);
|
||||
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
|
||||
return;
|
||||
case "/ipcamera.mpd":
|
||||
sendFile(ctx, httpRequest.uri(), "application/dash+xml");
|
||||
return;
|
||||
case "/ipcamera.gif":
|
||||
sendFile(ctx, httpRequest.uri(), "image/gif");
|
||||
return;
|
||||
case "/ipcamera.jpg":
|
||||
if (!ipCameraHandler.snapshotPolling && ipCameraHandler.snapshotUri != "") {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.snapshotUri);
|
||||
}
|
||||
if (ipCameraHandler.currentSnapshot.length == 1) {
|
||||
logger.warn("ipcamera.jpg was requested but there is no jpg in ram to send.");
|
||||
return;
|
||||
}
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
case "/snapshots.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.startSnapshotPolling();
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, false);
|
||||
return;
|
||||
case "/ipcamera.mjpeg":
|
||||
ipCameraHandler.setupMjpegStreaming(true, ctx);
|
||||
handlingMjpeg = true;
|
||||
return;
|
||||
case "/autofps.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, true);
|
||||
return;
|
||||
case "/instar":
|
||||
InstarHandler instar = new InstarHandler(ipCameraHandler);
|
||||
instar.alarmTriggered(httpRequest.uri().toString());
|
||||
ctx.close();
|
||||
return;
|
||||
case "/ipcamera0.ts":
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".gif")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/gif");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
// Allow access to the preroll and postroll jpg files
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/mp4");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if ("POST".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
switch (httpRequest.uri()) {
|
||||
case "/ipcamera.jpg":
|
||||
break;
|
||||
case "/snapshot.jpg":
|
||||
updateSnapshot = true;
|
||||
break;
|
||||
case "/OnvifEvent":
|
||||
onvifEvent = true;
|
||||
break;
|
||||
default:
|
||||
logger.debug("Stream Server recieved unknown request \tPOST:{}", httpRequest.uri());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
if (recievedBytes == 0) {
|
||||
incomingJpeg = new byte[content.content().readableBytes()];
|
||||
content.content().getBytes(0, incomingJpeg, 0, content.content().readableBytes());
|
||||
} else {
|
||||
byte[] temp = incomingJpeg;
|
||||
incomingJpeg = new byte[recievedBytes + content.content().readableBytes()];
|
||||
System.arraycopy(temp, 0, incomingJpeg, 0, temp.length);
|
||||
content.content().getBytes(0, incomingJpeg, temp.length, content.content().readableBytes());
|
||||
}
|
||||
recievedBytes = incomingJpeg.length;
|
||||
if (content instanceof LastHttpContent) {
|
||||
if (updateSnapshot) {
|
||||
ipCameraHandler.processSnapshot(incomingJpeg);
|
||||
} else if (onvifEvent) {
|
||||
ipCameraHandler.onvifCamera.eventRecieved(new String(incomingJpeg, StandardCharsets.UTF_8));
|
||||
} else { // handles the snapshots that make up mjpeg from rtsp to ffmpeg conversions.
|
||||
if (recievedBytes > 1000) {
|
||||
ipCameraHandler.sendMjpegFrame(incomingJpeg, ipCameraHandler.mjpegChannelGroup);
|
||||
}
|
||||
}
|
||||
recievedBytes = 0;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
ipCameraHandler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(ipCameraHandler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
ipCameraHandler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
File file = new File(ipCameraHandler.cameraConfig.getFfmpegOutput() + fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (ctx == null || cause == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.trace("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
if (handlingMjpeg) {
|
||||
ipCameraHandler.setupMjpegStreaming(false, ctx);
|
||||
} else if (handlingSnapshotStream) {
|
||||
handlingSnapshotStream = false;
|
||||
ipCameraHandler.setupSnapshotStreaming(false, ctx, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
@@ -32,30 +31,19 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.GroupConfig;
|
||||
import org.openhab.binding.ipcamera.internal.GroupTracker;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.StreamServerGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.servlet.GroupServlet;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
|
||||
* group picture.
|
||||
@@ -66,14 +54,13 @@ import io.netty.handler.timeout.IdleStateHandler;
|
||||
@NonNullByDefault
|
||||
public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private final HttpService httpService;
|
||||
public GroupConfig groupConfig;
|
||||
private BigDecimal pollTimeInSeconds = new BigDecimal(2);
|
||||
public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
|
||||
private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
|
||||
private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
|
||||
private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
|
||||
private @Nullable ServerBootstrap serverBootstrap;
|
||||
private @Nullable ChannelFuture serverFuture = null;
|
||||
private @Nullable GroupServlet servlet;
|
||||
public String hostIp;
|
||||
private boolean motionChangesOrder = true;
|
||||
public int serverPort = 0;
|
||||
@@ -86,7 +73,8 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
private int discontinuitySequence = 0;
|
||||
private GroupTracker groupTracker;
|
||||
|
||||
public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker) {
|
||||
public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker,
|
||||
HttpService httpService) {
|
||||
super(thing);
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
if (openhabIpAddress != null) {
|
||||
@@ -95,12 +83,26 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
hostIp = Helper.getLocalIpAddress();
|
||||
}
|
||||
this.groupTracker = groupTracker;
|
||||
this.httpService = httpService;
|
||||
}
|
||||
|
||||
public String getPlayList() {
|
||||
return playList;
|
||||
}
|
||||
|
||||
private int getNextIndex() {
|
||||
if (cameraIndex + 1 == cameraOrder.size()) {
|
||||
return 0;
|
||||
}
|
||||
return cameraIndex + 1;
|
||||
}
|
||||
|
||||
public byte[] getSnapshot() {
|
||||
// ask camera to fetch the next jpg ahead of time
|
||||
cameraOrder.get(getNextIndex()).getSnapshot();
|
||||
return cameraOrder.get(cameraIndex).getSnapshot();
|
||||
}
|
||||
|
||||
public String getOutputFolder(int index) {
|
||||
IpCameraHandler handle = cameraOrder.get(index);
|
||||
return handle.cameraConfig.getFfmpegOutput();
|
||||
@@ -165,65 +167,30 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
|
||||
public void createPlayList() {
|
||||
String m3u8File = readCamerasPlaylist(cameraIndex);
|
||||
if (m3u8File == "") {
|
||||
if (m3u8File.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int numberOfSegments = howManySegments(m3u8File);
|
||||
logger.debug("Using {} segmented files to make up a poll period.", numberOfSegments);
|
||||
logger.trace("Using {} segmented files to make up a poll period.", numberOfSegments);
|
||||
m3u8File = keepLast(m3u8File, numberOfSegments);
|
||||
m3u8File = m3u8File.replace("ipcamera", cameraIndex + "ipcamera"); // add index so we can then fetch output path
|
||||
if (entries > numberOfSegments * 3) {
|
||||
playingNow = removeFromStart(playingNow, entries - (numberOfSegments * 3));
|
||||
}
|
||||
playingNow = playingNow + "#EXT-X-DISCONTINUITY\n" + m3u8File;
|
||||
playList = "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:5\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
|
||||
+ discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n" + playingNow;
|
||||
playList = "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:6\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
|
||||
+ discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n"
|
||||
+ "#EXT-X-INDEPENDENT-SEGMENTS\n" + playingNow;
|
||||
}
|
||||
|
||||
private IpCameraGroupHandler getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void startStreamServer(boolean start) {
|
||||
if (!start) {
|
||||
serversLoopGroup.shutdownGracefully(8, 8, TimeUnit.SECONDS);
|
||||
serverBootstrap = null;
|
||||
} else {
|
||||
if (serverBootstrap == null) {
|
||||
try {
|
||||
serversLoopGroup = new NioEventLoopGroup();
|
||||
serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(serversLoopGroup);
|
||||
serverBootstrap.channel(NioServerSocketChannel.class);
|
||||
// IP "0.0.0.0" will bind the server to all network connections//
|
||||
serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", serverPort));
|
||||
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 25, 0));
|
||||
socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
|
||||
socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
|
||||
socketChannel.pipeline().addLast("streamServerHandler",
|
||||
new StreamServerGroupHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
serverFuture = serverBootstrap.bind().sync();
|
||||
serverFuture.await(4000);
|
||||
logger.info("IpCamera file server for a group of cameras has started on port {} for all NIC's.",
|
||||
serverPort);
|
||||
updateState(CHANNEL_MJPEG_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.mjpeg"));
|
||||
updateState(CHANNEL_HLS_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.jpg"));
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Exception occured when starting the streaming server. Try changing the serverPort to another number.");
|
||||
}
|
||||
}
|
||||
}
|
||||
public void startStreamServer() {
|
||||
servlet = new GroupServlet(this, httpService);
|
||||
updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/snapshots.mjpeg"));
|
||||
updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.jpg"));
|
||||
}
|
||||
|
||||
void addCamera(String UniqueID) {
|
||||
@@ -231,9 +198,9 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
|
||||
if (handler.getThing().getUID().getId().equals(UniqueID)) {
|
||||
if (!cameraOrder.contains(handler)) {
|
||||
logger.info("Adding {} to a camera group.", UniqueID);
|
||||
logger.debug("Adding {} to a camera group.", UniqueID);
|
||||
if (hlsTurnedOn) {
|
||||
logger.info("Starting HLS for the new camera.");
|
||||
logger.debug("Starting HLS for the new camera added to group.");
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
@@ -262,7 +229,7 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
// Event based. This is called as each camera comes online after the group handler is registered.
|
||||
public void cameraOffline(IpCameraHandler handle) {
|
||||
if (cameraOrder.remove(handle)) {
|
||||
logger.info("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
|
||||
logger.debug("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +277,12 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
if (motionChangesOrder) {
|
||||
cameraIndex = checkForMotion(cameraIndex);
|
||||
}
|
||||
GroupServlet localServlet = servlet;
|
||||
if (localServlet != null) {
|
||||
if (localServlet.snapshotStreamsOpen > 0) {
|
||||
cameraOrder.get(cameraIndex).getSnapshot();
|
||||
}
|
||||
}
|
||||
if (hlsTurnedOn) {
|
||||
discontinuitySequence++;
|
||||
createPlayList();
|
||||
@@ -339,19 +312,10 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
@Override
|
||||
public void initialize() {
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
serverPort = groupConfig.getServerPort();
|
||||
pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
|
||||
pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
|
||||
motionChangesOrder = groupConfig.getMotionChangesOrder();
|
||||
|
||||
if (serverPort == -1) {
|
||||
logger.warn("The serverPort = -1 which disables a lot of features. See readme for more info.");
|
||||
} else if (serverPort < 1025) {
|
||||
logger.warn("The serverPort is <= 1024 and may cause permission errors under Linux, try a higher port.");
|
||||
}
|
||||
if (groupConfig.getServerPort() > 0) {
|
||||
startStreamServer(true);
|
||||
}
|
||||
startStreamServer();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000,
|
||||
groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
|
||||
@@ -359,12 +323,15 @@ public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
startStreamServer(false);
|
||||
groupTracker.listOfGroupHandlers.remove(this);
|
||||
Future<?> future = pollCameraGroupJob;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
cameraOrder.clear();
|
||||
GroupServlet localServlet = servlet;
|
||||
if (localServlet != null) {
|
||||
localServlet.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.math.BigDecimal;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -56,8 +55,8 @@ import org.openhab.binding.ipcamera.internal.IpCameraActions;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
|
||||
import org.openhab.binding.ipcamera.internal.StreamServerHandler;
|
||||
import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
|
||||
import org.openhab.binding.ipcamera.internal.servlet.CameraServlet;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
@@ -74,11 +73,11 @@ import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
@@ -93,24 +92,18 @@ import io.netty.channel.group.ChannelGroup;
|
||||
import io.netty.channel.group.DefaultChannelGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.base64.Base64;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMessage;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
@@ -134,10 +127,10 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
public CameraConfig cameraConfig = new CameraConfig();
|
||||
|
||||
// ChannelGroup is thread safe
|
||||
public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
private final ChannelGroup snapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
private final ChannelGroup autoSnapshotMjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
public final ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||
private final HttpService httpService;
|
||||
private @Nullable CameraServlet servlet;
|
||||
public String mjpegContentType = "";
|
||||
public @Nullable Ffmpeg ffmpegHLS = null;
|
||||
public @Nullable Ffmpeg ffmpegRecord = null;
|
||||
public @Nullable Ffmpeg ffmpegGIF = null;
|
||||
@@ -151,10 +144,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
private @Nullable ScheduledFuture<?> pollCameraJob = null;
|
||||
private @Nullable ScheduledFuture<?> snapshotJob = null;
|
||||
private @Nullable Bootstrap mainBootstrap;
|
||||
private @Nullable ServerBootstrap serverBootstrap;
|
||||
|
||||
private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
|
||||
private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
|
||||
private FullHttpRequest putRequestWithBody = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
|
||||
"");
|
||||
private String gifFilename = "ipcamera";
|
||||
@@ -168,7 +158,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
private LinkedList<byte[]> fifoSnapshotBuffer = new LinkedList<byte[]>();
|
||||
private int snapCount;
|
||||
private boolean updateImageChannel = false;
|
||||
private boolean updateAutoFps = false;
|
||||
private byte lowPriorityCounter = 0;
|
||||
public String hostIp;
|
||||
public Map<String, ChannelTracking> channelTrackingMap = new ConcurrentHashMap<>();
|
||||
@@ -180,9 +169,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
public boolean useDigestAuth = false;
|
||||
public String snapshotUri = "";
|
||||
public String mjpegUri = "";
|
||||
private @Nullable ChannelFuture serverFuture = null;
|
||||
private Object firstStreamedMsg = new Object();
|
||||
public byte[] currentSnapshot = new byte[] { (byte) 0x00 };
|
||||
private byte[] currentSnapshot = new byte[] { (byte) 0x00 };
|
||||
public ReentrantLock lockCurrentSnapshot = new ReentrantLock();
|
||||
public String rtspUri = "";
|
||||
public boolean audioAlarmUpdateSnapshot = false;
|
||||
@@ -192,9 +179,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
private boolean firstMotionAlarm = false;
|
||||
public BigDecimal motionThreshold = BigDecimal.ZERO;
|
||||
public int audioThreshold = 35;
|
||||
@SuppressWarnings("unused")
|
||||
private @Nullable StreamServerHandler streamServerHandler;
|
||||
private boolean streamingSnapshotMjpeg = false;
|
||||
public boolean streamingSnapshotMjpeg = false;
|
||||
public boolean motionAlarmEnabled = false;
|
||||
public boolean audioAlarmEnabled = false;
|
||||
public boolean ffmpegSnapshotGeneration = false;
|
||||
@@ -254,9 +239,11 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
if (mjpegUri.equals(requestUrl)) {
|
||||
if (msg instanceof HttpMessage) {
|
||||
// very start of stream only
|
||||
ReferenceCountUtil.retain(msg, 1);
|
||||
firstStreamedMsg = msg;
|
||||
streamToGroup(firstStreamedMsg, mjpegChannelGroup, true);
|
||||
mjpegContentType = contentType;
|
||||
CameraServlet localServlet = servlet;
|
||||
if (localServlet != null) {
|
||||
localServlet.openStreams.updateContentType(contentType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
boundary = Helper.searchString(contentType, "boundary=");
|
||||
@@ -274,8 +261,13 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
if (msg instanceof HttpContent) {
|
||||
if (mjpegUri.equals(requestUrl)) {
|
||||
// multiple MJPEG stream packets come back as this.
|
||||
ReferenceCountUtil.retain(msg, 1);
|
||||
streamToGroup(msg, mjpegChannelGroup, true);
|
||||
HttpContent content = (HttpContent) msg;
|
||||
byte[] chunkedFrame = new byte[content.content().readableBytes()];
|
||||
content.content().getBytes(content.content().readerIndex(), chunkedFrame);
|
||||
CameraServlet localServlet = servlet;
|
||||
if (localServlet != null) {
|
||||
localServlet.openStreams.queueFrame(chunkedFrame);
|
||||
}
|
||||
} else {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
// Found some cameras use Content-Type: image/jpg instead of image/jpeg
|
||||
@@ -421,7 +413,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
|
||||
IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
|
||||
IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
|
||||
super(thing);
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
if (ipAddress != null) {
|
||||
@@ -430,6 +422,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
hostIp = Helper.getLocalIpAddress();
|
||||
}
|
||||
this.groupTracker = groupTracker;
|
||||
this.httpService = httpService;
|
||||
}
|
||||
|
||||
private IpCameraHandler getHandle() {
|
||||
@@ -520,6 +513,20 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
return httpRequestURL;
|
||||
}
|
||||
|
||||
private void checkCameraConnection() {
|
||||
Bootstrap localBootstrap = mainBootstrap;
|
||||
if (localBootstrap != null) {
|
||||
ChannelFuture chFuture = localBootstrap
|
||||
.connect(new InetSocketAddress(cameraConfig.getIp(), cameraConfig.getPort()));
|
||||
if (chFuture.awaitUninterruptibly(500)) {
|
||||
chFuture.channel().close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
cameraCommunicationError(
|
||||
"Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
|
||||
}
|
||||
|
||||
// Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
|
||||
// The authHandler will generate a digest string and re-send using this same function when needed.
|
||||
@SuppressWarnings("null")
|
||||
@@ -657,19 +664,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
lockCurrentSnapshot.unlock();
|
||||
}
|
||||
|
||||
if (streamingSnapshotMjpeg) {
|
||||
sendMjpegFrame(incommingSnapshot, snapshotMjpegChannelGroup);
|
||||
}
|
||||
if (streamingAutoFps) {
|
||||
if (motionDetected) {
|
||||
sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
|
||||
} else if (updateAutoFps) {
|
||||
// only happens every 8 seconds as some browsers need a frame that often to keep stream alive.
|
||||
sendMjpegFrame(incommingSnapshot, autoSnapshotMjpegChannelGroup);
|
||||
updateAutoFps = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateImageChannel) {
|
||||
updateState(CHANNEL_IMAGE, new RawType(incommingSnapshot, "image/jpeg"));
|
||||
} else if (firstMotionAlarm || motionAlarmUpdateSnapshot) {
|
||||
@@ -681,132 +675,27 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public void stopStreamServer() {
|
||||
serversLoopGroup.shutdownGracefully();
|
||||
serverBootstrap = null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void startStreamServer() {
|
||||
if (serverBootstrap == null) {
|
||||
try {
|
||||
serversLoopGroup = new NioEventLoopGroup();
|
||||
serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(serversLoopGroup);
|
||||
serverBootstrap.channel(NioServerSocketChannel.class);
|
||||
// IP "0.0.0.0" will bind the server to all network connections//
|
||||
serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", cameraConfig.getServerPort()));
|
||||
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 60, 0));
|
||||
socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
|
||||
socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
|
||||
socketChannel.pipeline().addLast("streamServerHandler", new StreamServerHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
serverFuture = serverBootstrap.bind().sync();
|
||||
serverFuture.await(4000);
|
||||
logger.debug("File server for camera at {} has started on port {} for all NIC's.", cameraConfig.getIp(),
|
||||
cameraConfig.getServerPort());
|
||||
updateState(CHANNEL_MJPEG_URL,
|
||||
new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
|
||||
updateState(CHANNEL_HLS_URL,
|
||||
new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL,
|
||||
new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
|
||||
} catch (Exception e) {
|
||||
cameraConfigError("Exception when starting server. Try changing the Server Port to another number.");
|
||||
}
|
||||
if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
|
||||
logger.debug("Setting up the Alarm Server settings in the camera now");
|
||||
sendHttpGET(
|
||||
"/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
|
||||
+ hostIp + "&-as_port=" + cameraConfig.getServerPort()
|
||||
+ "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
|
||||
}
|
||||
if (servlet == null) {
|
||||
servlet = new CameraServlet(this, httpService);
|
||||
}
|
||||
|
||||
updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.jpg"));
|
||||
updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.mjpeg"));
|
||||
}
|
||||
|
||||
public void setupSnapshotStreaming(boolean stream, ChannelHandlerContext ctx, boolean auto) {
|
||||
if (stream) {
|
||||
sendMjpegFirstPacket(ctx);
|
||||
if (auto) {
|
||||
autoSnapshotMjpegChannelGroup.add(ctx.channel());
|
||||
lockCurrentSnapshot.lock();
|
||||
try {
|
||||
sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
|
||||
// iOS uses a FIFO? and needs two frames to display a pic
|
||||
sendMjpegFrame(currentSnapshot, autoSnapshotMjpegChannelGroup);
|
||||
} finally {
|
||||
lockCurrentSnapshot.unlock();
|
||||
}
|
||||
streamingAutoFps = true;
|
||||
} else {
|
||||
snapshotMjpegChannelGroup.add(ctx.channel());
|
||||
lockCurrentSnapshot.lock();
|
||||
try {
|
||||
sendMjpegFrame(currentSnapshot, snapshotMjpegChannelGroup);
|
||||
} finally {
|
||||
lockCurrentSnapshot.unlock();
|
||||
}
|
||||
streamingSnapshotMjpeg = true;
|
||||
startSnapshotPolling();
|
||||
}
|
||||
} else {
|
||||
snapshotMjpegChannelGroup.remove(ctx.channel());
|
||||
autoSnapshotMjpegChannelGroup.remove(ctx.channel());
|
||||
if (streamingSnapshotMjpeg && snapshotMjpegChannelGroup.isEmpty()) {
|
||||
streamingSnapshotMjpeg = false;
|
||||
stopSnapshotPolling();
|
||||
logger.debug("All snapshots.mjpeg streams have stopped.");
|
||||
} else if (streamingAutoFps && autoSnapshotMjpegChannelGroup.isEmpty()) {
|
||||
streamingAutoFps = false;
|
||||
stopSnapshotPolling();
|
||||
logger.debug("All autofps.mjpeg streams have stopped.");
|
||||
}
|
||||
}
|
||||
public void openCamerasStream() {
|
||||
threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void openMjpegStream() {
|
||||
sendHttpGET(mjpegUri);
|
||||
}
|
||||
|
||||
// If start is true the CTX is added to the list to stream video to, false stops
|
||||
// the stream.
|
||||
public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) {
|
||||
if (start) {
|
||||
if (mjpegChannelGroup.isEmpty()) {// first stream being requested.
|
||||
mjpegChannelGroup.add(ctx.channel());
|
||||
if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) {
|
||||
sendMjpegFirstPacket(ctx);
|
||||
setupFfmpegFormat(FFmpegFormat.MJPEG);
|
||||
} else {// Delay fixes Dahua reboots when refreshing a mjpeg stream.
|
||||
threadPool.schedule(this::openMjpegStream, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} else if (ffmpegMjpeg != null) {// not first stream and we will use ffmpeg
|
||||
sendMjpegFirstPacket(ctx);
|
||||
mjpegChannelGroup.add(ctx.channel());
|
||||
} else {// not first stream and camera supplies the mjpeg source.
|
||||
ctx.channel().writeAndFlush(firstStreamedMsg);
|
||||
mjpegChannelGroup.add(ctx.channel());
|
||||
}
|
||||
} else {
|
||||
mjpegChannelGroup.remove(ctx.channel());
|
||||
if (mjpegChannelGroup.isEmpty()) {
|
||||
logger.debug("All ipcamera.mjpeg streams have stopped.");
|
||||
if ("ffmpeg".equals(mjpegUri) || mjpegUri.isEmpty()) {
|
||||
Ffmpeg localMjpeg = ffmpegMjpeg;
|
||||
if (localMjpeg != null) {
|
||||
localMjpeg.stopConverting();
|
||||
}
|
||||
} else {
|
||||
closeChannel(getTinyUrl(mjpegUri));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void openChannel(Channel channel, String httpRequestURL) {
|
||||
ChannelTracking tracker = channelTrackingMap.get(httpRequestURL);
|
||||
if (tracker != null && !tracker.getReply().isEmpty()) {// We need to keep the stored reply
|
||||
@@ -816,7 +705,7 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
channelTrackingMap.put(httpRequestURL, new ChannelTracking(channel, httpRequestURL));
|
||||
}
|
||||
|
||||
void closeChannel(String url) {
|
||||
public void closeChannel(String url) {
|
||||
ChannelTracking channelTracking = channelTrackingMap.get(url);
|
||||
if (channelTracking != null) {
|
||||
if (channelTracking.getChannel().isOpen()) {
|
||||
@@ -856,39 +745,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// sends direct to ctx so can be either snapshots.mjpeg or normal mjpeg stream
|
||||
public void sendMjpegFirstPacket(ChannelHandlerContext ctx) {
|
||||
final String boundary = "thisMjpegStream";
|
||||
String contentType = "multipart/x-mixed-replace; boundary=" + boundary;
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().writeAndFlush(response);
|
||||
}
|
||||
|
||||
public void sendMjpegFrame(byte[] jpg, ChannelGroup channelGroup) {
|
||||
final String boundary = "thisMjpegStream";
|
||||
ByteBuf imageByteBuf = Unpooled.copiedBuffer(jpg);
|
||||
int length = imageByteBuf.readableBytes();
|
||||
String header = "--" + boundary + "\r\n" + "content-type: image/jpeg" + "\r\n" + "content-length: " + length
|
||||
+ "\r\n\r\n";
|
||||
ByteBuf headerBbuf = Unpooled.copiedBuffer(header, 0, header.length(), StandardCharsets.UTF_8);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
streamToGroup(headerBbuf, channelGroup, false);
|
||||
streamToGroup(imageByteBuf, channelGroup, false);
|
||||
streamToGroup(footerBbuf, channelGroup, true);
|
||||
}
|
||||
|
||||
public void streamToGroup(Object msg, ChannelGroup channelGroup, boolean flush) {
|
||||
channelGroup.write(msg);
|
||||
if (flush) {
|
||||
channelGroup.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void storeSnapshots() {
|
||||
int count = 0;
|
||||
// Need to lock as fifoSnapshotBuffer is not thread safe and new snapshots can be incoming.
|
||||
@@ -1060,8 +916,8 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
inputOptions += " -hide_banner -loglevel warning";
|
||||
}
|
||||
ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
|
||||
cameraConfig.getMjpegOptions(),
|
||||
"http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
|
||||
cameraConfig.getMjpegOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.jpg",
|
||||
cameraConfig.getUser(), cameraConfig.getPassword());
|
||||
}
|
||||
Ffmpeg localMjpeg = ffmpegMjpeg;
|
||||
@@ -1079,8 +935,8 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
|
||||
}
|
||||
ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
|
||||
cameraConfig.getSnapshotOptions(),
|
||||
"http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
|
||||
cameraConfig.getSnapshotOptions(), "http://127.0.0.1:" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/snapshot.jpg",
|
||||
cameraConfig.getUser(), cameraConfig.getPassword());
|
||||
}
|
||||
Ffmpeg localSnaps = ffmpegSnapshot;
|
||||
@@ -1196,21 +1052,19 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void channelLinked(ChannelUID channelUID) {
|
||||
if (cameraConfig.getServerPort() > 0) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_MJPEG_URL:
|
||||
updateState(CHANNEL_MJPEG_URL, new StringType(
|
||||
"http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg"));
|
||||
break;
|
||||
case CHANNEL_HLS_URL:
|
||||
updateState(CHANNEL_HLS_URL,
|
||||
new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8"));
|
||||
break;
|
||||
case CHANNEL_IMAGE_URL:
|
||||
updateState(CHANNEL_IMAGE_URL,
|
||||
new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg"));
|
||||
break;
|
||||
}
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_MJPEG_URL:
|
||||
updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.mjpeg"));
|
||||
break;
|
||||
case CHANNEL_HLS_URL:
|
||||
updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.m3u8"));
|
||||
break;
|
||||
case CHANNEL_IMAGE_URL:
|
||||
updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ getThing().getUID().getId() + "/ipcamera.jpg"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1464,7 +1318,14 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
if (localFuture != null) {
|
||||
localFuture.cancel(false);
|
||||
}
|
||||
|
||||
if (thing.getThingTypeUID().getId().equals(INSTAR_THING)) {
|
||||
logger.debug("Setting up the Alarm Server settings in the camera now");
|
||||
sendHttpGET(
|
||||
"/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
|
||||
+ hostIp + "&-as_port=" + SERVLET_PORT + "&-as_path=/ipcamera/"
|
||||
+ getThing().getUID().getId()
|
||||
+ "/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
|
||||
}
|
||||
if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
|
||||
snapshotPolling = true;
|
||||
snapshotJob = threadPool.scheduleWithFixedDelay(this::snapshotRunnable, 1000, cameraConfig.getPollTime(),
|
||||
@@ -1566,6 +1427,18 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getSnapshot() {
|
||||
if (!snapshotPolling && !ffmpegSnapshotGeneration) {
|
||||
sendHttpGET(snapshotUri);
|
||||
}
|
||||
lockCurrentSnapshot.lock();
|
||||
try {
|
||||
return currentSnapshot;
|
||||
} finally {
|
||||
lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopSnapshotPolling() {
|
||||
Future<?> localFuture;
|
||||
if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
|
||||
@@ -1607,13 +1480,12 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
void pollCameraRunnable() {
|
||||
// Snapshot should be first to keep consistent time between shots
|
||||
if (streamingAutoFps) {
|
||||
updateAutoFps = true;
|
||||
if (!snapshotPolling && !ffmpegSnapshotGeneration) {
|
||||
// Dont need to poll if creating from RTSP stream with FFmpeg or we are polling at full rate already.
|
||||
sendHttpGET(snapshotUri);
|
||||
}
|
||||
} else if (!snapshotUri.isEmpty() && !snapshotPolling) {// we need to check camera is still online.
|
||||
sendHttpGET(snapshotUri);
|
||||
checkCameraConnection();
|
||||
}
|
||||
// NOTE: Use lowPriorityRequests if get request is not needed every poll.
|
||||
if (!lowPriorityRequests.isEmpty()) {
|
||||
@@ -1688,14 +1560,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
cameraConfig
|
||||
.setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/");
|
||||
}
|
||||
|
||||
if (cameraConfig.getServerPort() < 1) {
|
||||
logger.warn(
|
||||
"The Server Port is not set to a valid number which disables a lot of binding features. See readme for more info.");
|
||||
} else if (cameraConfig.getServerPort() < 1025) {
|
||||
logger.warn("The Server Port is <= 1024 and may cause permission errors under Linux, try a higher number.");
|
||||
}
|
||||
|
||||
// Known cameras will connect quicker if we skip ONVIF questions.
|
||||
switch (thing.getThingTypeUID().getId()) {
|
||||
case AMCREST_THING:
|
||||
@@ -1745,11 +1609,8 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Onvif and Instar event handling needs the host IP and the server started.
|
||||
if (cameraConfig.getServerPort() > 0) {
|
||||
startStreamServer();
|
||||
}
|
||||
// Onvif and Instar event handling need the host IP and the server started.
|
||||
startStreamServer();
|
||||
|
||||
if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)) {
|
||||
onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
|
||||
@@ -1801,7 +1662,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
}
|
||||
basicAuth = ""; // clear out stored Password hash
|
||||
useDigestAuth = false;
|
||||
stopStreamServer();
|
||||
openChannels.close();
|
||||
|
||||
Ffmpeg localFfmpeg = ffmpegHLS;
|
||||
@@ -1833,10 +1693,6 @@ public class IpCameraHandler extends BaseThingHandler {
|
||||
onvifCamera.disconnect();
|
||||
}
|
||||
|
||||
public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
|
||||
streamServerHandler = streamServerHandler2;
|
||||
}
|
||||
|
||||
public String getWhiteList() {
|
||||
return cameraConfig.getIpWhitelist();
|
||||
}
|
||||
|
||||
@@ -231,7 +231,8 @@ public class OnvifConnection {
|
||||
return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
|
||||
case Subscribe:
|
||||
return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
|
||||
+ ipCameraHandler.hostIp + ":" + ipCameraHandler.cameraConfig.getServerPort()
|
||||
+ ipCameraHandler.hostIp + ":" + SERVLET_PORT + "/ipcamera/"
|
||||
+ ipCameraHandler.getThing().getUID().getId()
|
||||
+ "/OnvifEvent</Address></ConsumerReference></Subscribe>";
|
||||
case Unsubscribe:
|
||||
return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
|
||||
@@ -849,7 +850,6 @@ public class OnvifConnection {
|
||||
logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
|
||||
} finally {
|
||||
logger.debug("Eventloop is shutdown:{}", mainEventLoopGroup.isShutdown());
|
||||
mainEventLoopGroup = new NioEventLoopGroup();
|
||||
bootstrap = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.servlet;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.HLS_STARTUP_DELAY_MS;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.Ffmpeg;
|
||||
import org.openhab.binding.ipcamera.internal.InstarHandler;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.osgi.service.http.HttpService;
|
||||
|
||||
/**
|
||||
* The {@link CameraServlet} is responsible for serving files for a single camera back to the Jetty server normally
|
||||
* found on port 8080
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CameraServlet extends IpCameraServlet {
|
||||
private static final long serialVersionUID = -134658667574L;
|
||||
private final IpCameraHandler handler;
|
||||
private int autofpsStreamsOpen = 0;
|
||||
private int snapshotStreamsOpen = 0;
|
||||
public OpenStreams openStreams = new OpenStreams();
|
||||
|
||||
public CameraServlet(IpCameraHandler handler, HttpService httpService) {
|
||||
super(handler, httpService);
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
|
||||
if (req == null || resp == null) {
|
||||
return;
|
||||
}
|
||||
String pathInfo = req.getPathInfo();
|
||||
if (pathInfo == null) {
|
||||
return;
|
||||
}
|
||||
switch (pathInfo) {
|
||||
case "/ipcamera.jpg":
|
||||
// ffmpeg sends data here for ipcamera.mjpeg streams when camera has no native stream.
|
||||
ServletInputStream snapshotData = req.getInputStream();
|
||||
openStreams.queueFrame(snapshotData.readAllBytes());
|
||||
snapshotData.close();
|
||||
break;
|
||||
case "/snapshot.jpg":
|
||||
snapshotData = req.getInputStream();
|
||||
handler.processSnapshot(snapshotData.readAllBytes());
|
||||
snapshotData.close();
|
||||
break;
|
||||
case "/OnvifEvent":
|
||||
handler.onvifCamera.eventRecieved(req.getReader().toString());
|
||||
break;
|
||||
default:
|
||||
logger.debug("Recieved unknown request \tPOST:{}", pathInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
|
||||
if (req == null || resp == null) {
|
||||
return;
|
||||
}
|
||||
String pathInfo = req.getPathInfo();
|
||||
if (pathInfo == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("GET:{}, received from {}", pathInfo, req.getRemoteHost());
|
||||
if (!"DISABLE".equals(handler.getWhiteList())) {
|
||||
String requestIP = "(" + req.getRemoteHost() + ")";
|
||||
if (!handler.getWhiteList().contains(requestIP)) {
|
||||
logger.warn("The request made from {} was not in the whiteList and will be ignored.", requestIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (pathInfo) {
|
||||
case "/ipcamera.m3u8":
|
||||
Ffmpeg localFfmpeg = handler.ffmpegHLS;
|
||||
if (localFfmpeg == null) {
|
||||
handler.setupFfmpegFormat(FFmpegFormat.HLS);
|
||||
} else if (!localFfmpeg.getIsAlive()) {
|
||||
localFfmpeg.startConverting();
|
||||
} else {
|
||||
localFfmpeg.setKeepAlive(8);
|
||||
sendFile(resp, pathInfo, "application/x-mpegURL");
|
||||
return;
|
||||
}
|
||||
// Allow files to be created, or you get old m3u8 from the last time this ran.
|
||||
try {
|
||||
Thread.sleep(HLS_STARTUP_DELAY_MS);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
sendFile(resp, pathInfo, "application/x-mpegURL");
|
||||
return;
|
||||
case "/ipcamera.mpd":
|
||||
sendFile(resp, pathInfo, "application/dash+xml");
|
||||
return;
|
||||
case "/ipcamera.gif":
|
||||
sendFile(resp, pathInfo, "image/gif");
|
||||
return;
|
||||
case "/ipcamera.jpg":
|
||||
sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
|
||||
return;
|
||||
case "/snapshots.mjpeg":
|
||||
req.getSession().setMaxInactiveInterval(0);
|
||||
snapshotStreamsOpen++;
|
||||
handler.streamingSnapshotMjpeg = true;
|
||||
handler.startSnapshotPolling();
|
||||
StreamOutput output = new StreamOutput(resp);
|
||||
do {
|
||||
try {
|
||||
output.sendSnapshotBasedFrame(handler.getSnapshot());
|
||||
Thread.sleep(1005);
|
||||
} catch (InterruptedException | IOException e) {
|
||||
// Never stop streaming until IOException. Occurs when browser stops the stream.
|
||||
snapshotStreamsOpen--;
|
||||
if (snapshotStreamsOpen == 0) {
|
||||
handler.streamingSnapshotMjpeg = false;
|
||||
handler.stopSnapshotPolling();
|
||||
logger.debug("All snapshots.mjpeg streams have stopped.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} while (true);
|
||||
case "/ipcamera.mjpeg":
|
||||
req.getSession().setMaxInactiveInterval(0);
|
||||
if (handler.mjpegUri.isEmpty() || "ffmpeg".equals(handler.mjpegUri)) {
|
||||
if (openStreams.isEmpty()) {
|
||||
handler.setupFfmpegFormat(FFmpegFormat.MJPEG);
|
||||
}
|
||||
output = new StreamOutput(resp);
|
||||
openStreams.addStream(output);
|
||||
} else if (openStreams.isEmpty()) {
|
||||
logger.debug("First stream requested, opening up stream from camera");
|
||||
handler.openCamerasStream();
|
||||
output = new StreamOutput(resp, handler.mjpegContentType);
|
||||
openStreams.addStream(output);
|
||||
} else {
|
||||
logger.debug("Not the first stream requested. Stream from camera already open");
|
||||
output = new StreamOutput(resp, handler.mjpegContentType);
|
||||
openStreams.addStream(output);
|
||||
}
|
||||
do {
|
||||
try {
|
||||
output.sendFrame();
|
||||
} catch (InterruptedException | IOException e) {
|
||||
// Never stop streaming until IOException. Occurs when browser stops the stream.
|
||||
openStreams.removeStream(output);
|
||||
if (openStreams.isEmpty()) {
|
||||
if (output.isSnapshotBased) {
|
||||
Ffmpeg localMjpeg = handler.ffmpegMjpeg;
|
||||
if (localMjpeg != null) {
|
||||
localMjpeg.stopConverting();
|
||||
}
|
||||
} else {
|
||||
handler.closeChannel(handler.getTinyUrl(handler.mjpegUri));
|
||||
}
|
||||
logger.debug("All ipcamera.mjpeg streams have stopped.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} while (true);
|
||||
case "/autofps.mjpeg":
|
||||
req.getSession().setMaxInactiveInterval(0);
|
||||
autofpsStreamsOpen++;
|
||||
handler.streamingAutoFps = true;
|
||||
output = new StreamOutput(resp);
|
||||
int counter = 0;
|
||||
do {
|
||||
try {
|
||||
if (handler.motionDetected) {
|
||||
output.sendSnapshotBasedFrame(handler.getSnapshot());
|
||||
} // every 8 seconds if no motion or the first three snapshots to fill any FIFO
|
||||
else if (counter % 8 == 0 || counter < 3) {
|
||||
output.sendSnapshotBasedFrame(handler.getSnapshot());
|
||||
}
|
||||
counter++;
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException | IOException e) {
|
||||
// Never stop streaming until IOException. Occurs when browser stops the stream.
|
||||
autofpsStreamsOpen--;
|
||||
if (autofpsStreamsOpen == 0) {
|
||||
handler.streamingAutoFps = false;
|
||||
logger.debug("All autofps.mjpeg streams have stopped.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} while (true);
|
||||
case "/instar":
|
||||
InstarHandler instar = new InstarHandler(handler);
|
||||
instar.alarmTriggered(pathInfo + "?" + req.getQueryString());
|
||||
return;
|
||||
default:
|
||||
if (pathInfo.endsWith(".ts")) {
|
||||
sendFile(resp, pathInfo, "video/MP2T");
|
||||
} else if (pathInfo.endsWith(".gif")) {
|
||||
sendFile(resp, pathInfo, "image/gif");
|
||||
} else if (pathInfo.endsWith(".jpg")) {
|
||||
// Allow access to the preroll and postroll jpg files
|
||||
sendFile(resp, pathInfo, "image/jpg");
|
||||
} else if (pathInfo.endsWith(".mp4")) {
|
||||
sendFile(resp, pathInfo, "video/mp4");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
|
||||
// Ensure no files can be sourced from parent or child folders
|
||||
String truncated = filename.substring(filename.lastIndexOf("/"));
|
||||
super.sendFile(response, handler.cameraConfig.getFfmpegOutput() + truncated, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
openStreams.closeAllStreams();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.servlet;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.osgi.service.http.HttpService;
|
||||
|
||||
/**
|
||||
* The {@link GroupServlet} is responsible for serving files for a rotating feed of multiple cameras back to the Jetty
|
||||
* server normally found on port 8080
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GroupServlet extends IpCameraServlet {
|
||||
private static final long serialVersionUID = -234658667574L;
|
||||
private final IpCameraGroupHandler handler;
|
||||
public int snapshotStreamsOpen = 0;
|
||||
|
||||
public GroupServlet(IpCameraGroupHandler handler, HttpService httpService) {
|
||||
super(handler, httpService);
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
|
||||
if (req == null || resp == null) {
|
||||
return;
|
||||
}
|
||||
String pathInfo = req.getPathInfo();
|
||||
if (pathInfo == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("GET:{}, received from {}", pathInfo, req.getRemoteHost());
|
||||
if (!"DISABLE".equals(handler.groupConfig.getIpWhitelist())) {
|
||||
String requestIP = "(" + req.getRemoteHost() + ")";
|
||||
if (!handler.groupConfig.getIpWhitelist().contains(requestIP)) {
|
||||
logger.warn("The request made from {} was not in the whiteList and will be ignored.", requestIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (pathInfo) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (!handler.hlsTurnedOn) {
|
||||
logger.debug(
|
||||
"HLS requires the groups startStream channel to be turned on first. Just starting it now.");
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(HLS_STARTUP_DELAY_MS);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
String playList = handler.getPlayList();
|
||||
sendString(resp, playList, "application/x-mpegURL");
|
||||
return;
|
||||
case "/ipcamera.jpg":
|
||||
sendSnapshotImage(resp, "image/jpg", handler.getSnapshot());
|
||||
return;
|
||||
case "/ipcamera.mjpeg":
|
||||
case "/snapshots.mjpeg":
|
||||
req.getSession().setMaxInactiveInterval(0);
|
||||
snapshotStreamsOpen++;
|
||||
StreamOutput output = new StreamOutput(resp);
|
||||
do {
|
||||
try {
|
||||
output.sendSnapshotBasedFrame(handler.getSnapshot());
|
||||
Thread.sleep(1005);
|
||||
} catch (InterruptedException | IOException e) {
|
||||
// Never stop streaming until IOException. Occurs when browser stops the stream.
|
||||
snapshotStreamsOpen--;
|
||||
if (snapshotStreamsOpen == 0) {
|
||||
logger.debug("All snapshots.mjpeg streams have stopped.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} while (true);
|
||||
default:
|
||||
// example is "/1ipcameraxx.ts"
|
||||
if (pathInfo.endsWith(".ts")) {
|
||||
sendFile(resp, pathInfo, "video/MP2T");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveIndexToPath(String uri) {
|
||||
if (!"i".equals(uri.substring(1, 2))) {
|
||||
return handler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
|
||||
}
|
||||
return "notFound";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
|
||||
// Ensure no files can be sourced from parent or child folders
|
||||
String truncated = filename.substring(filename.lastIndexOf("/"));
|
||||
truncated = resolveIndexToPath(truncated) + truncated.substring(2);
|
||||
File file = new File(truncated);
|
||||
if (!file.exists()) {
|
||||
logger.warn(
|
||||
"HLS File {} was not found. Try adding a larger -hls_delete_threshold to each cameras HLS out options.",
|
||||
file.getName());
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
super.sendFile(response, truncated, contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendSnapshotImage(HttpServletResponse response, String contentType, byte[] snapshot) {
|
||||
if (handler.cameraIndex >= handler.cameraOrder.size()) {
|
||||
logger.debug("All cameras in this group are OFFLINE and a snapshot was requested.");
|
||||
return;
|
||||
}
|
||||
super.sendSnapshotImage(response, contentType, snapshot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.servlet;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraServlet} is responsible for serving files to the Jetty
|
||||
* server normally found on port 8080
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class IpCameraServlet extends HttpServlet {
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private static final long serialVersionUID = 1L;
|
||||
protected final ThingHandler handler;
|
||||
protected final HttpService httpService;
|
||||
|
||||
public IpCameraServlet(ThingHandler handler, HttpService httpService) {
|
||||
this.handler = handler;
|
||||
this.httpService = httpService;
|
||||
startListening();
|
||||
}
|
||||
|
||||
public void startListening() {
|
||||
try {
|
||||
httpService.registerServlet("/ipcamera/" + handler.getThing().getUID().getId(), this, null,
|
||||
httpService.createDefaultHttpContext());
|
||||
} catch (NamespaceException | ServletException e) {
|
||||
logger.warn("Registering servlet failed:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendSnapshotImage(HttpServletResponse response, String contentType, byte[] snapshot) {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Expose-Headers", "*");
|
||||
response.setContentType(contentType);
|
||||
if (snapshot.length == 1) {
|
||||
logger.warn("ipcamera.jpg was requested but there was no jpg in ram to send.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
response.setContentLength(snapshot.length);
|
||||
ServletOutputStream servletOut = response.getOutputStream();
|
||||
servletOut.write(snapshot);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendString(HttpServletResponse response, String contents, String contentType) {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Expose-Headers", "*");
|
||||
response.setContentType(contentType);
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Cache-Control", "max-age=0, no-cache, no-store");
|
||||
byte[] bytes = contents.getBytes();
|
||||
try {
|
||||
response.setContentLength(bytes.length);
|
||||
ServletOutputStream servletOut = response.getOutputStream();
|
||||
servletOut.write(bytes);
|
||||
servletOut.write("\r\n".getBytes());
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendFile(HttpServletResponse response, String filename, String contentType) throws IOException {
|
||||
File file = new File(filename);
|
||||
if (!file.exists()) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
response.setBufferSize((int) file.length());
|
||||
response.setContentType(contentType);
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Expose-Headers", "*");
|
||||
response.setHeader("Content-Length", String.valueOf(file.length()));
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Cache-Control", "max-age=0, no-cache, no-store");
|
||||
BufferedInputStream input = null;
|
||||
BufferedOutputStream output = null;
|
||||
try {
|
||||
input = new BufferedInputStream(new FileInputStream(file), (int) file.length());
|
||||
output = new BufferedOutputStream(response.getOutputStream(), (int) file.length());
|
||||
byte[] buffer = new byte[(int) file.length()];
|
||||
int length;
|
||||
while ((length = input.read(buffer)) > 0) {
|
||||
output.write(buffer, 0, length);
|
||||
}
|
||||
} finally {
|
||||
if (output != null) {
|
||||
output.close();
|
||||
}
|
||||
if (input != null) {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
try {
|
||||
httpService.unregister("/ipcamera/" + handler.getThing().getUID().getId());
|
||||
this.destroy();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Unregistration of servlet failed:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.servlet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link OpenStreams} Keeps track of all open mjpeg streams so the byte[] can be given to all FIFO buffers to allow
|
||||
* 1 to many streams without needing to open more than 1 source stream.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OpenStreams {
|
||||
private List<StreamOutput> openStreams = Collections.synchronizedList(new ArrayList<StreamOutput>());
|
||||
|
||||
public synchronized void addStream(StreamOutput stream) {
|
||||
openStreams.add(stream);
|
||||
}
|
||||
|
||||
public synchronized void removeStream(StreamOutput stream) {
|
||||
openStreams.remove(stream);
|
||||
}
|
||||
|
||||
public synchronized int getNumberOfStreams() {
|
||||
return openStreams.size();
|
||||
}
|
||||
|
||||
public synchronized boolean isEmpty() {
|
||||
return openStreams.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized void updateContentType(String contentType) {
|
||||
for (StreamOutput stream : openStreams) {
|
||||
stream.updateContentType(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void queueFrame(byte[] frame) {
|
||||
for (StreamOutput stream : openStreams) {
|
||||
stream.queueFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void closeAllStreams() {
|
||||
for (StreamOutput stream : openStreams) {
|
||||
stream.close();
|
||||
}
|
||||
openStreams.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link StreamOutput} Streams mjpeg out to a client
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamOutput {
|
||||
private final HttpServletResponse response;
|
||||
private final String boundary;
|
||||
private String contentType;
|
||||
private final ServletOutputStream output;
|
||||
private BlockingQueue<byte[]> fifo = new ArrayBlockingQueue<byte[]>(6);
|
||||
private boolean connected = false;
|
||||
public boolean isSnapshotBased = false;
|
||||
|
||||
public StreamOutput(HttpServletResponse response) throws IOException {
|
||||
boundary = "thisMjpegStream";
|
||||
contentType = "multipart/x-mixed-replace; boundary=" + boundary;
|
||||
this.response = response;
|
||||
output = response.getOutputStream();
|
||||
isSnapshotBased = true;
|
||||
}
|
||||
|
||||
public StreamOutput(HttpServletResponse response, String contentType) throws IOException {
|
||||
boundary = "";
|
||||
this.contentType = contentType;
|
||||
this.response = response;
|
||||
output = response.getOutputStream();
|
||||
if (!contentType.isEmpty()) {
|
||||
sendInitialHeaders();
|
||||
connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendSnapshotBasedFrame(byte[] currentSnapshot) throws IOException {
|
||||
String header = "--" + boundary + "\r\n" + "Content-Type: image/jpeg" + "\r\n" + "Content-Length: "
|
||||
+ currentSnapshot.length + "\r\n\r\n";
|
||||
if (!connected) {
|
||||
sendInitialHeaders();
|
||||
// iOS needs to have two jpgs sent for the picture to appear instantly.
|
||||
output.write(header.getBytes());
|
||||
output.write(currentSnapshot);
|
||||
output.write("\r\n".getBytes());
|
||||
connected = true;
|
||||
}
|
||||
output.write(header.getBytes());
|
||||
output.write(currentSnapshot);
|
||||
output.write("\r\n".getBytes());
|
||||
}
|
||||
|
||||
public void queueFrame(byte[] frame) {
|
||||
fifo.add(frame);
|
||||
}
|
||||
|
||||
public void updateContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
if (!connected) {
|
||||
sendInitialHeaders();
|
||||
connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendFrame() throws IOException, InterruptedException {
|
||||
if (isSnapshotBased) {
|
||||
sendSnapshotBasedFrame(fifo.take());
|
||||
} else if (connected) {
|
||||
output.write(fifo.take());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendInitialHeaders() {
|
||||
response.setContentType(contentType);
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Expose-Headers", "*");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,13 +82,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -291,13 +284,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique for each
|
||||
camera.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -558,13 +544,6 @@
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -772,13 +751,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -1145,13 +1117,6 @@
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -1344,13 +1309,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -1615,13 +1573,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -1911,13 +1862,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
@@ -2194,13 +2138,6 @@
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serverPort" type="integer" required="true" min="-1" max="65535" groupName="Settings">
|
||||
<label>Server Port</label>
|
||||
<description>The port that will serve any files back to openHAB without authentication. It must be unique and unused
|
||||
for each camera. Setting the port to -1 will turn the feature off.
|
||||
</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="ipWhitelist" type="text" required="false" groupName="Settings">
|
||||
<label>IP Whitelist</label>
|
||||
<description>Enter any IP's inside (brackets) that you wish to allow to access the video stream. 'DISABLE' will
|
||||
|
||||
Reference in New Issue
Block a user