]> git.basschouten.com Git - openhab-addons.git/blob
b56abd9e7efa4f63efb8c3b5457aef553228e6e9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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
54 @NonNullByDefault
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;
64     public String hostIp;
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;
75
76     public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker,
77             HttpService httpService) {
78         super(thing);
79         groupConfig = getConfigAs(GroupConfig.class);
80         if (openhabIpAddress != null) {
81             hostIp = openhabIpAddress;
82         } else {
83             hostIp = Helper.getLocalIpAddress();
84         }
85         this.groupTracker = groupTracker;
86         this.httpService = httpService;
87     }
88
89     public String getPlayList() {
90         return playList;
91     }
92
93     private int getNextIndex() {
94         if (cameraIndex + 1 == cameraOrder.size()) {
95             return 0;
96         }
97         return cameraIndex + 1;
98     }
99
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();
104     }
105
106     public String getOutputFolder(int index) {
107         IpCameraHandler handle = cameraOrder.get(index);
108         return handle.cameraConfig.getFfmpegOutput();
109     }
110
111     private String readCamerasPlaylist(int cameraIndex) {
112         String camerasm3u8 = "";
113         IpCameraHandler handle = cameraOrder.get(cameraIndex);
114         try {
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());
119         }
120         return camerasm3u8;
121     }
122
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);
127             if (start == -1) {
128                 logger.warn(
129                         "Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
130                 return "";
131             }
132         }
133         entries = entries + numberToRetain;
134         return string.substring(start);
135     }
136
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) {
142                 logger.warn(
143                         "Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
144                 return string;
145             }
146         }
147         mediaSequence = mediaSequence + numberToRemove;
148         entries = entries - numberToRemove;
149         return string.substring(startingFrom);
150     }
151
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);
158             if (start != -1) {
159                 totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
160             } else {
161                 logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
162                 break;
163             }
164         }
165         return numberOfFiles;
166     }
167
168     public void createPlayList() {
169         String m3u8File = readCamerasPlaylist(cameraIndex);
170         if (m3u8File.isEmpty()) {
171             return;
172         }
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));
179         }
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;
184     }
185
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"));
194     }
195
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);
202                         if (hlsTurnedOn) {
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);
207                         }
208                         cameraOrder.add(handler);
209                     }
210                 }
211             }
212         }
213     }
214
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)) {
219             addCamera(uid);
220         } else if (groupConfig.getSecondCamera().equals(uid)) {
221             addCamera(uid);
222         } else if (groupConfig.getThirdCamera().equals(uid)) {
223             addCamera(uid);
224         } else if (groupConfig.getForthCamera().equals(uid)) {
225             addCamera(uid);
226         }
227     }
228
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());
233         }
234     }
235
236     boolean addIfOnline(String UniqueID) {
237         if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
238             addCamera(UniqueID);
239             return true;
240         }
241         return false;
242     }
243
244     void createCameraOrder() {
245         addIfOnline(groupConfig.getFirstCamera());
246         addIfOnline(groupConfig.getSecondCamera());
247         if (!groupConfig.getThirdCamera().isEmpty()) {
248             addIfOnline(groupConfig.getThirdCamera());
249         }
250         if (!groupConfig.getForthCamera().isEmpty()) {
251             addIfOnline(groupConfig.getForthCamera());
252         }
253         // Cameras can now send events of when they go on and offline.
254         groupTracker.listOfGroupHandlers.add(this);
255     }
256
257     int checkForMotion(int nextCamerasIndex) {
258         int checked = 0;
259         for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
260             if (cameraOrder.get(index).motionDetected) {
261                 return index;
262             }
263             if (++index >= cameraOrder.size()) {
264                 index = 0;
265             }
266         }
267         return nextCamerasIndex;
268     }
269
270     void pollCameraGroup() {
271         if (cameraOrder.isEmpty()) {
272             createCameraOrder();
273         }
274         if (++cameraIndex >= cameraOrder.size()) {
275             cameraIndex = 0;
276         }
277         if (motionChangesOrder) {
278             cameraIndex = checkForMotion(cameraIndex);
279         }
280         GroupServlet localServlet = servlet;
281         if (localServlet != null) {
282             if (localServlet.snapshotStreamsOpen > 0) {
283                 cameraOrder.get(cameraIndex).getSnapshot();
284             }
285         }
286         if (hlsTurnedOn) {
287             discontinuitySequence++;
288             createPlayList();
289         }
290     }
291
292     @Override
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)) {
298                         hlsTurnedOn = true;
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);
303                         }
304                     } else {
305                         // Do we turn all controls OFF, or do we remember the state before we turned them all on?
306                         hlsTurnedOn = false;
307                     }
308             }
309         }
310     }
311
312     @Override
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();
318         startStreamServer();
319         updateStatus(ThingStatus.ONLINE);
320         pollCameraGroupJob = pollCameraGroup.scheduleWithFixedDelay(this::pollCameraGroup, 10000,
321                 groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
322     }
323
324     @Override
325     public void dispose() {
326         groupTracker.listOfGroupHandlers.remove(this);
327         Future<?> future = pollCameraGroupJob;
328         if (future != null) {
329             future.cancel(true);
330         }
331         cameraOrder.clear();
332         GroupServlet localServlet = servlet;
333         if (localServlet != null) {
334             localServlet.dispose();
335         }
336     }
337 }