]> git.basschouten.com Git - openhab-addons.git/blob
02eedf594f4af90dd4ed35f8c4d2f5cef772d2dc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal.handler;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
16
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;
28
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;
46
47 /**
48  * The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
49  * group picture.
50  *
51  * @author Matthew Skinner - Initial contribution
52  */
53 @NonNullByDefault
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;
63     public String hostIp;
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;
74
75     public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker,
76             HttpService httpService) {
77         super(thing);
78         groupConfig = getConfigAs(GroupConfig.class);
79         if (openhabIpAddress != null) {
80             hostIp = openhabIpAddress;
81         } else {
82             hostIp = Helper.getLocalIpAddress();
83         }
84         this.groupTracker = groupTracker;
85         this.httpService = httpService;
86     }
87
88     public String getPlayList() {
89         return playList;
90     }
91
92     private int getNextIndex() {
93         if (cameraIndex + 1 == cameraOrder.size()) {
94             return 0;
95         }
96         return cameraIndex + 1;
97     }
98
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();
103     }
104
105     public String getOutputFolder(int index) {
106         IpCameraHandler handle = cameraOrder.get(index);
107         return handle.cameraConfig.getFfmpegOutput();
108     }
109
110     private String readCamerasPlaylist(int cameraIndex) {
111         String camerasm3u8 = "";
112         IpCameraHandler handle = cameraOrder.get(cameraIndex);
113         try {
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());
118         }
119         return camerasm3u8;
120     }
121
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);
126             if (start == -1) {
127                 logger.warn(
128                         "Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
129                 return "";
130             }
131         }
132         entries = entries + numberToRetain;
133         return string.substring(start);
134     }
135
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) {
141                 logger.warn(
142                         "Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
143                 return string;
144             }
145         }
146         mediaSequence = mediaSequence + numberToRemove;
147         entries = entries - numberToRemove;
148         return string.substring(startingFrom);
149     }
150
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);
157             if (start != -1) {
158                 totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
159             } else {
160                 logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
161                 break;
162             }
163         }
164         return numberOfFiles;
165     }
166
167     public void createPlayList() {
168         String m3u8File = readCamerasPlaylist(cameraIndex);
169         if (m3u8File.isEmpty()) {
170             return;
171         }
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));
178         }
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;
183     }
184
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"));
193     }
194
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);
201                         if (hlsTurnedOn) {
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);
206                         }
207                         cameraOrder.add(handler);
208                     }
209                 }
210             }
211         }
212     }
213
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)) {
218             addCamera(uid);
219         } else if (groupConfig.getSecondCamera().equals(uid)) {
220             addCamera(uid);
221         } else if (groupConfig.getThirdCamera().equals(uid)) {
222             addCamera(uid);
223         } else if (groupConfig.getForthCamera().equals(uid)) {
224             addCamera(uid);
225         }
226     }
227
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());
232         }
233     }
234
235     boolean addIfOnline(String UniqueID) {
236         if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
237             addCamera(UniqueID);
238             return true;
239         }
240         return false;
241     }
242
243     void createCameraOrder() {
244         addIfOnline(groupConfig.getFirstCamera());
245         addIfOnline(groupConfig.getSecondCamera());
246         if (!groupConfig.getThirdCamera().isEmpty()) {
247             addIfOnline(groupConfig.getThirdCamera());
248         }
249         if (!groupConfig.getForthCamera().isEmpty()) {
250             addIfOnline(groupConfig.getForthCamera());
251         }
252         // Cameras can now send events of when they go on and offline.
253         groupTracker.listOfGroupHandlers.add(this);
254     }
255
256     int checkForMotion(int nextCamerasIndex) {
257         int checked = 0;
258         for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
259             if (cameraOrder.get(index).motionDetected) {
260                 return index;
261             }
262             if (++index >= cameraOrder.size()) {
263                 index = 0;
264             }
265         }
266         return nextCamerasIndex;
267     }
268
269     void pollCameraGroup() {
270         if (cameraOrder.isEmpty()) {
271             createCameraOrder();
272         }
273         if (++cameraIndex >= cameraOrder.size()) {
274             cameraIndex = 0;
275         }
276         if (motionChangesOrder) {
277             cameraIndex = checkForMotion(cameraIndex);
278         }
279         GroupServlet localServlet = servlet;
280         if (localServlet != null) {
281             if (localServlet.snapshotStreamsOpen > 0) {
282                 cameraOrder.get(cameraIndex).getSnapshot();
283             }
284         }
285         if (hlsTurnedOn) {
286             discontinuitySequence++;
287             createPlayList();
288         }
289     }
290
291     @Override
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)) {
297                         hlsTurnedOn = true;
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);
302                         }
303                     } else {
304                         // Do we turn all controls OFF, or do we remember the state before we turned them all on?
305                         hlsTurnedOn = false;
306                     }
307             }
308         }
309     }
310
311     @Override
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();
317         startStreamServer();
318         updateStatus(ThingStatus.ONLINE);
319         pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000,
320                 groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
321     }
322
323     @Override
324     public void dispose() {
325         groupTracker.listOfGroupHandlers.remove(this);
326         Future<?> future = pollCameraGroupJob;
327         if (future != null) {
328             future.cancel(true);
329         }
330         cameraOrder.clear();
331         GroupServlet localServlet = servlet;
332         if (localServlet != null) {
333             localServlet.dispose();
334         }
335     }
336 }