2 * Copyright (c) 2010-2024 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
54 public class IpCameraGroupHandler extends BaseThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(getClass());
56 private final HttpService httpService;
57 public GroupConfig groupConfig;
58 private BigDecimal pollTimeInSeconds = new BigDecimal(2);
59 public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
60 private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
61 private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
62 private @Nullable GroupServlet servlet;
64 private boolean motionChangesOrder = true;
65 public int serverPort = 0;
66 public String playList = "";
67 private String playingNow = "";
68 public int cameraIndex = 0;
69 public boolean hlsTurnedOn = false;
70 private int entries = 0;
71 private int mediaSequence = 1;
72 private int discontinuitySequence = 0;
73 private GroupTracker groupTracker;
75 public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker,
76 HttpService httpService) {
78 groupConfig = getConfigAs(GroupConfig.class);
79 if (openhabIpAddress != null) {
80 hostIp = openhabIpAddress;
82 hostIp = Helper.getLocalIpAddress();
84 this.groupTracker = groupTracker;
85 this.httpService = httpService;
88 public String getPlayList() {
92 private int getNextIndex() {
93 if (cameraIndex + 1 == cameraOrder.size()) {
96 return cameraIndex + 1;
99 public byte[] getSnapshot() {
100 // ask camera to fetch the next jpg ahead of time
101 cameraOrder.get(getNextIndex()).getSnapshot();
102 return cameraOrder.get(cameraIndex).getSnapshot();
105 public String getOutputFolder(int index) {
106 IpCameraHandler handle = cameraOrder.get(index);
107 return handle.cameraConfig.getFfmpegOutput();
110 private String readCamerasPlaylist(int cameraIndex) {
111 String camerasm3u8 = "";
112 IpCameraHandler handle = cameraOrder.get(cameraIndex);
114 String file = handle.cameraConfig.getFfmpegOutput() + "ipcamera.m3u8";
115 camerasm3u8 = new String(Files.readAllBytes(Paths.get(file)));
116 } catch (IOException e) {
117 logger.warn("Error occured fetching a groupDisplay cameras m3u8 file :{}", e.getMessage());
122 String keepLast(String string, int numberToRetain) {
123 int start = string.length();
124 for (int loop = numberToRetain; loop > 0; loop--) {
125 start = string.lastIndexOf("#EXTINF:", start - 1);
128 "Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
132 entries = entries + numberToRetain;
133 return string.substring(start);
136 String removeFromStart(String string, int numberToRemove) {
137 int startingFrom = string.indexOf("#EXTINF:");
138 for (int loop = numberToRemove; loop > 0; loop--) {
139 startingFrom = string.indexOf("#EXTINF:", startingFrom + 27);
140 if (startingFrom == -1) {
142 "Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
146 mediaSequence = mediaSequence + numberToRemove;
147 entries = entries - numberToRemove;
148 return string.substring(startingFrom);
151 int howManySegments(String m3u8File) {
152 int start = m3u8File.length();
153 int numberOfFiles = 0;
154 for (BigDecimal totalTime = new BigDecimal(0); totalTime.intValue() < pollTimeInSeconds
155 .intValue(); numberOfFiles++) {
156 start = m3u8File.lastIndexOf("#EXTINF:", start - 1);
158 totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
160 logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
164 return numberOfFiles;
167 public void createPlayList() {
168 String m3u8File = readCamerasPlaylist(cameraIndex);
169 if (m3u8File.isEmpty()) {
172 int numberOfSegments = howManySegments(m3u8File);
173 logger.trace("Using {} segmented files to make up a poll period.", numberOfSegments);
174 m3u8File = keepLast(m3u8File, numberOfSegments);
175 m3u8File = m3u8File.replace("ipcamera", cameraIndex + "ipcamera"); // add index so we can then fetch output path
176 if (entries > numberOfSegments * 3) {
177 playingNow = removeFromStart(playingNow, entries - (numberOfSegments * 3));
179 playingNow = playingNow + "#EXT-X-DISCONTINUITY\n" + m3u8File;
180 playList = "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:6\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
181 + discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n"
182 + "#EXT-X-INDEPENDENT-SEGMENTS\n" + playingNow;
185 public void startStreamServer() {
186 servlet = new GroupServlet(this, httpService);
187 updateState(CHANNEL_MJPEG_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
188 + getThing().getUID().getId() + "/snapshots.mjpeg"));
189 updateState(CHANNEL_HLS_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
190 + getThing().getUID().getId() + "/ipcamera.m3u8"));
191 updateState(CHANNEL_IMAGE_URL, new StringType("http://" + hostIp + ":" + SERVLET_PORT + "/ipcamera/"
192 + getThing().getUID().getId() + "/ipcamera.jpg"));
195 void addCamera(String UniqueID) {
196 if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
197 for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
198 if (handler.getThing().getUID().getId().equals(UniqueID)) {
199 if (!cameraOrder.contains(handler)) {
200 logger.debug("Adding {} to a camera group.", UniqueID);
202 logger.debug("Starting HLS for the new camera added to group.");
203 String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
204 + handler.getThing().getUID().getId() + ":";
205 handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
207 cameraOrder.add(handler);
214 // Event based. This is called as each camera comes online after the group handler is registered.
215 public void cameraOnline(String uid) {
216 logger.debug("New camera {} came online, checking if part of this group", uid);
217 if (groupConfig.getFirstCamera().equals(uid)) {
219 } else if (groupConfig.getSecondCamera().equals(uid)) {
221 } else if (groupConfig.getThirdCamera().equals(uid)) {
223 } else if (groupConfig.getForthCamera().equals(uid)) {
228 // Event based. This is called as each camera comes online after the group handler is registered.
229 public void cameraOffline(IpCameraHandler handle) {
230 if (cameraOrder.remove(handle)) {
231 logger.debug("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
235 boolean addIfOnline(String UniqueID) {
236 if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
243 void createCameraOrder() {
244 addIfOnline(groupConfig.getFirstCamera());
245 addIfOnline(groupConfig.getSecondCamera());
246 if (!groupConfig.getThirdCamera().isEmpty()) {
247 addIfOnline(groupConfig.getThirdCamera());
249 if (!groupConfig.getForthCamera().isEmpty()) {
250 addIfOnline(groupConfig.getForthCamera());
252 // Cameras can now send events of when they go on and offline.
253 groupTracker.listOfGroupHandlers.add(this);
256 int checkForMotion(int nextCamerasIndex) {
258 for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
259 if (cameraOrder.get(index).motionDetected) {
262 if (++index >= cameraOrder.size()) {
266 return nextCamerasIndex;
269 void pollCameraGroup() {
270 if (cameraOrder.isEmpty()) {
273 if (++cameraIndex >= cameraOrder.size()) {
276 if (motionChangesOrder) {
277 cameraIndex = checkForMotion(cameraIndex);
279 GroupServlet localServlet = servlet;
280 if (localServlet != null) {
281 if (localServlet.snapshotStreamsOpen > 0) {
282 cameraOrder.get(cameraIndex).getSnapshot();
286 discontinuitySequence++;
292 public void handleCommand(ChannelUID channelUID, Command command) {
293 if (!(command instanceof RefreshType)) {
294 switch (channelUID.getId()) {
295 case CHANNEL_START_STREAM:
296 if (OnOffType.ON.equals(command)) {
298 for (IpCameraHandler handler : cameraOrder) {
299 String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
300 + handler.getThing().getUID().getId() + ":";
301 handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
304 // Do we turn all controls OFF, or do we remember the state before we turned them all on?
312 public void initialize() {
313 groupConfig = getConfigAs(GroupConfig.class);
314 pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
315 pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
316 motionChangesOrder = groupConfig.getMotionChangesOrder();
318 updateStatus(ThingStatus.ONLINE);
319 pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000,
320 groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
324 public void dispose() {
325 groupTracker.listOfGroupHandlers.remove(this);
326 Future<?> future = pollCameraGroupJob;
327 if (future != null) {
331 GroupServlet localServlet = servlet;
332 if (localServlet != null) {
333 localServlet.dispose();