2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.handler;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.nio.file.Files;
21 import java.nio.file.Paths;
22 import java.util.ArrayList;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.Future;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.ipcamera.internal.GroupConfig;
32 import org.openhab.binding.ipcamera.internal.GroupTracker;
33 import org.openhab.binding.ipcamera.internal.Helper;
34 import org.openhab.binding.ipcamera.internal.servlet.GroupServlet;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.osgi.service.http.HttpService;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
51 * @author Matthew Skinner - Initial contribution
55 public class IpCameraGroupHandler extends BaseThingHandler {
56 private final Logger logger = LoggerFactory.getLogger(getClass());
57 private final HttpService httpService;
58 public GroupConfig groupConfig;
59 private BigDecimal pollTimeInSeconds = new BigDecimal(2);
60 public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
61 private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
62 private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
63 private @Nullable GroupServlet servlet;
65 private boolean motionChangesOrder = true;
66 public int serverPort = 0;
67 public String playList = "";
68 private String playingNow = "";
69 public int cameraIndex = 0;
70 public boolean hlsTurnedOn = false;
71 private int entries = 0;
72 private int mediaSequence = 1;
73 private int discontinuitySequence = 0;
74 private GroupTracker groupTracker;
76 public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker,
77 HttpService httpService) {
79 groupConfig = getConfigAs(GroupConfig.class);
80 if (openhabIpAddress != null) {
81 hostIp = openhabIpAddress;
83 hostIp = Helper.getLocalIpAddress();
85 this.groupTracker = groupTracker;
86 this.httpService = httpService;
89 public String getPlayList() {
93 private int getNextIndex() {
94 if (cameraIndex + 1 == cameraOrder.size()) {
97 return cameraIndex + 1;
100 public byte[] getSnapshot() {
101 // ask camera to fetch the next jpg ahead of time
102 cameraOrder.get(getNextIndex()).getSnapshot();
103 return cameraOrder.get(cameraIndex).getSnapshot();
106 public String getOutputFolder(int index) {
107 IpCameraHandler handle = cameraOrder.get(index);
108 return handle.cameraConfig.getFfmpegOutput();
111 private String readCamerasPlaylist(int cameraIndex) {
112 String camerasm3u8 = "";
113 IpCameraHandler handle = cameraOrder.get(cameraIndex);
115 String file = handle.cameraConfig.getFfmpegOutput() + "ipcamera.m3u8";
116 camerasm3u8 = new String(Files.readAllBytes(Paths.get(file)));
117 } catch (IOException e) {
118 logger.warn("Error occured fetching a groupDisplay cameras m3u8 file :{}", e.getMessage());
123 String keepLast(String string, int numberToRetain) {
124 int start = string.length();
125 for (int loop = numberToRetain; loop > 0; loop--) {
126 start = string.lastIndexOf("#EXTINF:", start - 1);
129 "Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
133 entries = entries + numberToRetain;
134 return string.substring(start);
137 String removeFromStart(String string, int numberToRemove) {
138 int startingFrom = string.indexOf("#EXTINF:");
139 for (int loop = numberToRemove; loop > 0; loop--) {
140 startingFrom = string.indexOf("#EXTINF:", startingFrom + 27);
141 if (startingFrom == -1) {
143 "Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
147 mediaSequence = mediaSequence + numberToRemove;
148 entries = entries - numberToRemove;
149 return string.substring(startingFrom);
152 int howManySegments(String m3u8File) {
153 int start = m3u8File.length();
154 int numberOfFiles = 0;
155 for (BigDecimal totalTime = new BigDecimal(0); totalTime.intValue() < pollTimeInSeconds
156 .intValue(); numberOfFiles++) {
157 start = m3u8File.lastIndexOf("#EXTINF:", start - 1);
159 totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
161 logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
165 return numberOfFiles;
168 public void createPlayList() {
169 String m3u8File = readCamerasPlaylist(cameraIndex);
170 if (m3u8File.isEmpty()) {
173 int numberOfSegments = howManySegments(m3u8File);
174 logger.trace("Using {} segmented files to make up a poll period.", numberOfSegments);
175 m3u8File = keepLast(m3u8File, numberOfSegments);
176 m3u8File = m3u8File.replace("ipcamera", cameraIndex + "ipcamera"); // add index so we can then fetch output path
177 if (entries > numberOfSegments * 3) {
178 playingNow = removeFromStart(playingNow, entries - (numberOfSegments * 3));
180 playingNow = playingNow + "#EXT-X-DISCONTINUITY\n" + m3u8File;
181 playList = "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:6\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
182 + discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n"
183 + "#EXT-X-INDEPENDENT-SEGMENTS\n" + playingNow;
186 public void startStreamServer() {
187 servlet = new GroupServlet(this, httpService);
188 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
189 + getThing().getUID().getId() + "/snapshots.mjpeg"));
190 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
191 + getThing().getUID().getId() + "/ipcamera.m3u8"));
192 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
193 + getThing().getUID().getId() + "/ipcamera.jpg"));
196 void addCamera(String UniqueID) {
197 if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
198 for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
199 if (handler.getThing().getUID().getId().equals(UniqueID)) {
200 if (!cameraOrder.contains(handler)) {
201 logger.debug("Adding {} to a camera group.", UniqueID);
203 logger.debug("Starting HLS for the new camera added to group.");
204 String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
205 + handler.getThing().getUID().getId() + ":";
206 handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
208 cameraOrder.add(handler);
215 // Event based. This is called as each camera comes online after the group handler is registered.
216 public void cameraOnline(String uid) {
217 logger.debug("New camera {} came online, checking if part of this group", uid);
218 if (groupConfig.getFirstCamera().equals(uid)) {
220 } else if (groupConfig.getSecondCamera().equals(uid)) {
222 } else if (groupConfig.getThirdCamera().equals(uid)) {
224 } else if (groupConfig.getForthCamera().equals(uid)) {
229 // Event based. This is called as each camera comes online after the group handler is registered.
230 public void cameraOffline(IpCameraHandler handle) {
231 if (cameraOrder.remove(handle)) {
232 logger.debug("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
236 boolean addIfOnline(String UniqueID) {
237 if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
244 void createCameraOrder() {
245 addIfOnline(groupConfig.getFirstCamera());
246 addIfOnline(groupConfig.getSecondCamera());
247 if (!groupConfig.getThirdCamera().isEmpty()) {
248 addIfOnline(groupConfig.getThirdCamera());
250 if (!groupConfig.getForthCamera().isEmpty()) {
251 addIfOnline(groupConfig.getForthCamera());
253 // Cameras can now send events of when they go on and offline.
254 groupTracker.listOfGroupHandlers.add(this);
257 int checkForMotion(int nextCamerasIndex) {
259 for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
260 if (cameraOrder.get(index).motionDetected) {
263 if (++index >= cameraOrder.size()) {
267 return nextCamerasIndex;
270 void pollCameraGroup() {
271 if (cameraOrder.isEmpty()) {
274 if (++cameraIndex >= cameraOrder.size()) {
277 if (motionChangesOrder) {
278 cameraIndex = checkForMotion(cameraIndex);
280 GroupServlet localServlet = servlet;
281 if (localServlet != null) {
282 if (localServlet.snapshotStreamsOpen > 0) {
283 cameraOrder.get(cameraIndex).getSnapshot();
287 discontinuitySequence++;
293 public void handleCommand(ChannelUID channelUID, Command command) {
294 if (!(command instanceof RefreshType)) {
295 switch (channelUID.getId()) {
296 case CHANNEL_START_STREAM:
297 if (OnOffType.ON.equals(command)) {
299 for (IpCameraHandler handler : cameraOrder) {
300 String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
301 + handler.getThing().getUID().getId() + ":";
302 handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
305 // Do we turn all controls OFF, or do we remember the state before we turned them all on?
313 public void initialize() {
314 groupConfig = getConfigAs(GroupConfig.class);
315 pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
316 pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
317 motionChangesOrder = groupConfig.getMotionChangesOrder();
319 updateStatus(ThingStatus.ONLINE);
320 pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000,
321 groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
325 public void dispose() {
326 groupTracker.listOfGroupHandlers.remove(this);
327 Future<?> future = pollCameraGroupJob;
328 if (future != null) {
332 GroupServlet localServlet = servlet;
333 if (localServlet != null) {
334 localServlet.dispose();