* 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>
```
Thing ipcamera:generic:Esp32Cam
[
- ipAddress="192.168.1.181", serverPort=54322,
+ ipAddress="192.168.1.181",
gifPreroll=1,
snapshotUrl="http://192.168.1.181/capture",
mjpegUrl="http://192.168.1.181:81/stream",
onvifPort=8000, //normally 80 check what it needs
port=80,
nvrChannel=4,
- serverPort=54324,
ffmpegOutput="/var/lib/openhab/ipcamera/West/",
ffmpegInput="rtsp://192.168.0.XX:554/ISAPI/Streaming/channels/401"
]
## Thing Configuration
-After a camera is added, the first step is to provide login details and a valid serverPort for your camera before it will come online.
+After a camera is added, the first step is to provide login details for your camera before it will come online.
If your camera is not ONVIF/API based, you will also need to provide the binding with the cameras URLs to the relevant config field/s.
For ONVIF cameras that auto detect the wrong URL, these same fields can be used to force a URL of your choosing but leaving them blank will allow the binding to find the URL for you.
| `ipAddress`| The IP address or host name of your camera. |
| `port`| This port will be used for HTTP calls for fetching the snapshot and any API calls. |
| `onvifPort`| The port your camera uses for ONVIF connections. This is needed for PTZ movement, events, and the auto discovery of RTSP and snapshot URLs. |
-| `serverPort`| The port that will serve the video streams and snapshots back to openHAB without authentication. You can choose any number, but it must be unique and unused for each camera that you setup. Setting the port to -1 (default), will turn all file serving off and some features will fail to work. |
| `username`| Leave blank if your camera does not use login details. |
| `password`| Leave blank if your camera does not use login details. |
| `onvifMediaProfile`| 0 (default) is your cameras Mainstream and the numbers above 0 are the substreams. Any auto discovered URLs will use the streams that this indicates. You can always override the URLs should you wish to use something different for one of them. |
+ Use the cameras URL so it passes from the camera directly to your end device. ie a tablet.
This is always the best option if it works.
-+ Request a snapshot with the URL `http://192.168.xxx.xxx:54321/ipcamera.jpg`.
-The IP is for your openHAB server not the camera, and 54321 is the `serverPort` number that you specified in the bindings setup. If you find the snapshot is old, you can set the `gifPreroll` to a number above 0 and this forces the camera to keep updating the stored JPG in RAM.
++ Request a snapshot with the URL `http://openhabIP:8080/ipcamera/{cameraUID}/ipcamera.jpg`.
+The IP is for your openHAB server not the camera.
+If you find the snapshot is old, you can set the `gifPreroll` to a number above 0 and this forces the camera to keep updating the stored JPG in RAM.
The ipcamera.jpg can also be cast, as most cameras can not directly cast their snapshots.
-+ Use the `http://192.168.xxx.xxx:54321/snapshots.mjpeg` to request a stream of snapshots to be delivered in MJPEG format.
++ Use the `http://openHAB:8080/ipcamera/{cameraUID}/snapshots.mjpeg` to request a stream of snapshots to be delivered in MJPEG format.
+ Use the record GIF action and use a `gifPreroll` value > 0.
This creates a number of snapshots in the FFmpeg output folder called snapshotXXX.jpg where XXX starts at 0 and increases each `pollTime`.
This allows you to get a snapshot from an exact amount of time before, on, or after starting the record to GIF action.
Handy for cameras which lag due to slow processors, or if you do not want a hand blocking the image when the door bell was pushed.
These snapshots can be fetched either directly as they exist on disk, or via this URL format.
-`http://192.168.xxx.xxx:54321/snapshot0.jpg`
+`http://openHAB:8080/ipcamera/{cameraUID}/snapshot0.jpg`
+ Also worth a mention is that you can off load cameras to a software package running on a separate server such as, Motion, Shinobi and Zoneminder.
See this forum thread for examples of how to use snapshots and streams in a sitemap.
**IMPORTANT:**
The binding has its own file server that works by allowing access to the snapshot and video streams with no user/password for requests that come from an IP located in the `ipWhitelist`.
Requests from external IPs or internal requests that are not on the `ipWhitelist` will fail to get any answer.
-If you prefer to use your own firewall instead, you can also choose to make the `ipWhitelist` equal "DISABLE" (the default since the feature also needs a valid serverPort set) to turn this feature off and then all internal IPs will have access.
+If you prefer to use your own firewall instead, you can also choose to make the `ipWhitelist` equal "DISABLE" and then all internal IPs will have access.
-There are multiple ways to get a moving picture, to use them just enter the URL into any browser using `http://192.168.xxx.xxx:serverPort/name.format` replacing the name.format with one of the options that are listed below:
+There are multiple ways to get a moving picture, to use them just enter the URL into any browser using `http://openHAB:8080/ipcamera/{cameraUID}/name.format` replacing the name.format with one of the options that are listed below:
+ **ipcamera.m3u8** HLS (HTTP Live Streaming) which uses H.264 compression.
This can be used to cast to Chromecast devices, or can display video in many browsers (some browsers require a plugin to be installed).
To set this up, see [Special Notes for Different Brands](#special-notes-for-different-brands).
The binding can then distribute this stream to many devices around your home whilst the camera only sees a single open stream.
-To request the MJPEG stream from the binding, all you need to do is use this link changing the IP to that of your openHAB server and the serverPort to match the settings in the bindings setup for that camera.
+To request the MJPEG stream from the binding, all you need to do is use this link changing the IP to that of your openHAB server and the uniqueID of the camera.
-<http://openHABIP:serverPort/ipcamera.mjpeg>
+<http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.mjpeg>
**Creating MJPEG with FFmpeg**
This means lower traffic unless the picture is actually changing.
Request the stream to be sent to an item with this URL.
-NOTE: The IP is openHAB's not your cameras IP and the 54321 is what you have set as the serverPort.
+NOTE: The IP is openHAB's not your cameras IP.
-`http://192.168.xxx.xxx:54321/snapshots.mjpeg`
+`http://openHAB:8080/ipcamera/{cameraUID}/snapshots.mjpeg`
Use the following to display it in your sitemap.
```
-Video url="http://192.168.0.32:54321/autofps.mjpeg" encoding="mjpeg"
+Video url="http://openHAB:8080/ipcamera/{cameraUID}/autofps.mjpeg" encoding="mjpeg"
-Video url="http://192.168.0.32:54321/snapshots.mjpeg" encoding="mjpeg"
+Video url="http://openHAB:8080/ipcamera/{cameraUID}/snapshots.mjpeg" encoding="mjpeg"
```
## HLS (HTTP Live Streaming)
If the channel is OFF, the stream will start and stop automatically as required and the channel will reflect its current status.
With a fast openHAB server it should only need to be requested once, but on slower ARM systems it takes a while for FFmpeg to get up and running at full speed.
-It can be helpful sometimes to use this line in a rule to start the stream before it is needed further on in the rule `sendHttpGetRequest("http://192.168.0.2:54321/ipcamera.m3u8")` as the stream will stay running for 64 seconds.
+It can be helpful sometimes to use this line in a rule to start the stream before it is needed further on in the rule `sendHttpGetRequest("http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8")` as the stream will stay running for 64 seconds.
This 64 second delay before the stream is stopped helps when you are moving back and forth in a UI, as the stream does not keep stopping and needing to start each time you move around in a UI.
To use the HLS feature, you need to:
+ Ensure FFmpeg is installed.
+ For `generic` cameras, you will need to use the config `ffmpegInput` to provide a HTTP or RTSP URL.
-+ Set a valid `serverPort` as the value of -1 will turn this feature off.
+ Consider using a SSD/HDD, zram location, or a tmpfs (ram drive) can be used if you only have micro SD/flash based storage.
### Ram Drive Setup
```
-Text label="HLS Video Stream" icon="camera"{Video url="http://192.168.1.9:54321/ipcamera.m3u8" encoding="hls"}
+Text label="HLS Video Stream" icon="camera"{Video url="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" encoding="hls"}
-Text label="HLS Webview Stream" icon="camera"{Webview url="http://192.168.1.9:54321/ipcamera.m3u8" height=15}
+Text label="HLS Webview Stream" icon="camera"{Webview url="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" height=15}
```
<html>
<body>
<div style="width: 50%; float: left;">
- <video playsinline autoplay muted controls style="width:100%; " src="http://192.168.6.4:50001/ipcamera.m3u8" />
+ <video playsinline autoplay muted controls style="width:100%; " src="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" />
</div>
<div style="width: 50%; float: left;">
- <video playsinline autoplay muted controls style="width: 100%; " src="http://192.168.6.4:50002/ipcamera.m3u8" />
+ <video playsinline autoplay muted controls style="width: 100%; " src="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" />
</div>
<div style="width: 50%; float: left;">
- <video playsinline autoplay muted controls style="width:100%; " src="http://192.168.6.4:50003/ipcamera.m3u8" />
+ <video playsinline autoplay muted controls style="width:100%; " src="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" />
</div>
<div style="width: 50%; float: left;">
- <video playsinline autoplay muted controls style="width: 100%; " src="http://192.168.6.4:50004/ipcamera.m3u8" />
+ <video playsinline autoplay muted controls style="width: 100%; " src="http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8" />
</div>
</body>
</html>
In a rule...
```
-KitchenHomeHubPlayURI.sendCommand("http://192.168.1.2:54321/ipcamera.m3u8")
+KitchenHomeHubPlayURI.sendCommand("http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.m3u8")
```
You can request the GIF and MP4 by using this URL format, or by the direct path to where the file is stored:
-<http://openHAB.IP:serverPort/ipcamera.gif>
+<http://openHAB:8080/ipcamera/{cameraUID}/ipcamera.gif>
.items
If you have 3 seconds worth of video segments in each cameras HLS stream, this is the max you can set the poll time of the group to.
+ All cameras in a group should have the same HLS segment size setting, 1 and 2 second long segments have been tested to work.
+ Mixing cameras with different aspect ratios may cause issues when cast.
++ The HLS files need to remain on disk for the number of cameras X pollTime, use the `-hls_delete_threshold` ffmpeg option to control this.
## Sitemap Example
Slider item=BabyCam_Zoom icon=zoom
}
Default item=BabyCam_StartHLSStream
- Text label="Mjpeg Stream" icon="camera"{Video url="http://192.168.0.2:54321/ipcamera.mjpeg" encoding="mjpeg"}
- Text label="HLS Stream" icon="camera"{Webview url="http://192.168.0.2:54321/ipcamera.m3u8" height=15}
- Video url="http://192.168.0.2:54321/autofps.mjpeg" encoding="mjpeg"
+ Text label="Mjpeg Stream" icon="camera"{Video url="http://openHAB:8080/ipcamera/BabyCam/ipcamera.mjpeg" encoding="mjpeg"}
+ Text label="HLS Stream" icon="camera"{Webview url="http://openHAB:8080/ipcamera/BabyCam/ipcamera.m3u8" height=15}
+ Video url="http://openHAB:8080/ipcamera/BabyCam/autofps.mjpeg" encoding="mjpeg"
}
```
private String ffmpegInputOptions = "";
private int port;
private int onvifPort;
- private int serverPort;
private String username = "";
private String password = "";
private int onvifMediaProfile;
return onvifPort;
}
- public int getServerPort() {
- return serverPort;
- }
-
public String getIp() {
return ipAddress;
}
*/
@NonNullByDefault
public class GroupConfig {
- private int pollTime, serverPort;
+ private int pollTime;
private boolean motionChangesOrder = true;
private String ipWhitelist = "";
private String ffmpegLocation = "";
return ffmpegOutput;
}
- public int getServerPort() {
- return serverPort;
- }
-
public int getPollTime() {
return pollTime;
}
*/
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;
@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<>();
}
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;
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) {
}
}
- 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
}
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");
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
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
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;
}
+++ /dev/null
-/**
- * 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();
- }
-}
+++ /dev/null
-/**
- * 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);
- }
- }
-}
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;
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.
@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;
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) {
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();
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) {
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);
// 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());
}
}
if (motionChangesOrder) {
cameraIndex = checkForMotion(cameraIndex);
}
+ GroupServlet localServlet = servlet;
+ if (localServlet != null) {
+ if (localServlet.snapshotStreamsOpen > 0) {
+ cameraOrder.get(cameraIndex).getSnapshot();
+ }
+ }
if (hlsTurnedOn) {
discontinuitySequence++;
createPlayList();
@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);
@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();
+ }
}
}
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;
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;
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;
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;
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;
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";
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<>();
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;
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;
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=");
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
}
public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
- IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
+ IpCameraDynamicStateDescriptionProvider stateDescriptionProvider, HttpService httpService) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
if (ipAddress != null) {
hostIp = Helper.getLocalIpAddress();
}
this.groupTracker = groupTracker;
+ this.httpService = httpService;
}
private IpCameraHandler getHandle() {
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")
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) {
}
}
- 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
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()) {
}
}
- // 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.
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;
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;
@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;
}
}
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(),
}
}
+ 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
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()) {
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:
}
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(),
}
basicAuth = ""; // clear out stored Password hash
useDigestAuth = false;
- stopStreamServer();
openChannels.close();
Ffmpeg localFfmpeg = ffmpegHLS;
onvifCamera.disconnect();
}
- public void setStreamServerHandler(StreamServerHandler streamServerHandler2) {
- streamServerHandler = streamServerHandler2;
- }
-
public String getWhiteList() {
return cameraConfig.getIpWhitelist();
}
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>";
logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
} finally {
logger.debug("Eventloop is shutdown:{}", mainEventLoopGroup.isShutdown());
- mainEventLoopGroup = new NioEventLoopGroup();
bootstrap = null;
}
}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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) {
+ }
+ }
+}
<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
<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
<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
<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
</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
<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
<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
<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
<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