[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:
Matthew Skinner
2021-09-22 03:39:46 +10:00
committed by GitHub
parent 20f8a56560
commit fd646a59bd
18 changed files with 875 additions and 954 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -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

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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