]> git.basschouten.com Git - openhab-addons.git/blob
6fc3f2e1eb5f030b0d5fedd0c4158a6756da9738
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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
14 package org.openhab.binding.ipcamera.internal.handler;
15
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.math.RoundingMode;
21 import java.net.InetSocketAddress;
22 import java.nio.file.Files;
23 import java.nio.file.Paths;
24 import java.util.ArrayList;
25 import java.util.concurrent.Executors;
26 import java.util.concurrent.Future;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.ipcamera.internal.GroupConfig;
34 import org.openhab.binding.ipcamera.internal.GroupTracker;
35 import org.openhab.binding.ipcamera.internal.Helper;
36 import org.openhab.binding.ipcamera.internal.StreamServerGroupHandler;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import io.netty.bootstrap.ServerBootstrap;
50 import io.netty.channel.ChannelFuture;
51 import io.netty.channel.ChannelInitializer;
52 import io.netty.channel.EventLoopGroup;
53 import io.netty.channel.nio.NioEventLoopGroup;
54 import io.netty.channel.socket.SocketChannel;
55 import io.netty.channel.socket.nio.NioServerSocketChannel;
56 import io.netty.handler.codec.http.HttpServerCodec;
57 import io.netty.handler.stream.ChunkedWriteHandler;
58 import io.netty.handler.timeout.IdleStateHandler;
59
60 /**
61  * The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
62  * group picture.
63  *
64  * @author Matthew Skinner - Initial contribution
65  */
66
67 @NonNullByDefault
68 public class IpCameraGroupHandler extends BaseThingHandler {
69     private final Logger logger = LoggerFactory.getLogger(getClass());
70     public GroupConfig groupConfig;
71     private BigDecimal pollTimeInSeconds = new BigDecimal(2);
72     public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
73     private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
74     private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
75     private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
76     private @Nullable ServerBootstrap serverBootstrap;
77     private @Nullable ChannelFuture serverFuture = null;
78     public String hostIp;
79     private boolean motionChangesOrder = true;
80     public int serverPort = 0;
81     public String playList = "";
82     private String playingNow = "";
83     public int cameraIndex = 0;
84     public boolean hlsTurnedOn = false;
85     private int entries = 0;
86     private int mediaSequence = 1;
87     private int discontinuitySequence = 0;
88     private GroupTracker groupTracker;
89
90     public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker) {
91         super(thing);
92         groupConfig = getConfigAs(GroupConfig.class);
93         if (openhabIpAddress != null) {
94             hostIp = openhabIpAddress;
95         } else {
96             hostIp = Helper.getLocalIpAddress();
97         }
98         this.groupTracker = groupTracker;
99     }
100
101     public String getPlayList() {
102         return playList;
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 == "") {
170             return;
171         }
172         int numberOfSegments = howManySegments(m3u8File);
173         logger.debug("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:6\n#EXT-X-TARGETDURATION:5\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
181                 + discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n" + playingNow;
182     }
183
184     private IpCameraGroupHandler getHandle() {
185         return this;
186     }
187
188     @SuppressWarnings("null")
189     public void startStreamServer(boolean start) {
190         if (!start) {
191             serversLoopGroup.shutdownGracefully(8, 8, TimeUnit.SECONDS);
192             serverBootstrap = null;
193         } else {
194             if (serverBootstrap == null) {
195                 try {
196                     serversLoopGroup = new NioEventLoopGroup();
197                     serverBootstrap = new ServerBootstrap();
198                     serverBootstrap.group(serversLoopGroup);
199                     serverBootstrap.channel(NioServerSocketChannel.class);
200                     // IP "0.0.0.0" will bind the server to all network connections//
201                     serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", serverPort));
202                     serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
203                         @Override
204                         protected void initChannel(SocketChannel socketChannel) throws Exception {
205                             socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 25, 0));
206                             socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
207                             socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
208                             socketChannel.pipeline().addLast("streamServerHandler",
209                                     new StreamServerGroupHandler(getHandle()));
210                         }
211                     });
212                     serverFuture = serverBootstrap.bind().sync();
213                     serverFuture.await(4000);
214                     logger.info("IpCamera file server for a group of cameras has started on port {} for all NIC's.",
215                             serverPort);
216                     updateState(CHANNEL_MJPEG_URL,
217                             new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.mjpeg"));
218                     updateState(CHANNEL_HLS_URL,
219                             new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.m3u8"));
220                     updateState(CHANNEL_IMAGE_URL,
221                             new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.jpg"));
222                 } catch (Exception e) {
223                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
224                             "Exception occured when starting the streaming server. Try changing the serverPort to another number.");
225                 }
226             }
227         }
228     }
229
230     void addCamera(String UniqueID) {
231         if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
232             for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
233                 if (handler.getThing().getUID().getId().equals(UniqueID)) {
234                     if (!cameraOrder.contains(handler)) {
235                         logger.info("Adding {} to a camera group.", UniqueID);
236                         if (hlsTurnedOn) {
237                             logger.info("Starting HLS for the new camera.");
238                             String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
239                                     + handler.getThing().getUID().getId() + ":";
240                             handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
241                         }
242                         cameraOrder.add(handler);
243                     }
244                 }
245             }
246         }
247     }
248
249     // Event based. This is called as each camera comes online after the group handler is registered.
250     public void cameraOnline(String uid) {
251         logger.debug("New camera {} came online, checking if part of this group", uid);
252         if (groupConfig.getFirstCamera().equals(uid)) {
253             addCamera(uid);
254         } else if (groupConfig.getSecondCamera().equals(uid)) {
255             addCamera(uid);
256         } else if (groupConfig.getThirdCamera().equals(uid)) {
257             addCamera(uid);
258         } else if (groupConfig.getForthCamera().equals(uid)) {
259             addCamera(uid);
260         }
261     }
262
263     // Event based. This is called as each camera comes online after the group handler is registered.
264     public void cameraOffline(IpCameraHandler handle) {
265         if (cameraOrder.remove(handle)) {
266             logger.info("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
267         }
268     }
269
270     boolean addIfOnline(String UniqueID) {
271         if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
272             addCamera(UniqueID);
273             return true;
274         }
275         return false;
276     }
277
278     void createCameraOrder() {
279         addIfOnline(groupConfig.getFirstCamera());
280         addIfOnline(groupConfig.getSecondCamera());
281         if (!groupConfig.getThirdCamera().isEmpty()) {
282             addIfOnline(groupConfig.getThirdCamera());
283         }
284         if (!groupConfig.getForthCamera().isEmpty()) {
285             addIfOnline(groupConfig.getForthCamera());
286         }
287         // Cameras can now send events of when they go on and offline.
288         groupTracker.listOfGroupHandlers.add(this);
289     }
290
291     int checkForMotion(int nextCamerasIndex) {
292         int checked = 0;
293         for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
294             if (cameraOrder.get(index).motionDetected) {
295                 return index;
296             }
297             if (++index >= cameraOrder.size()) {
298                 index = 0;
299             }
300         }
301         return nextCamerasIndex;
302     }
303
304     void pollCameraGroup() {
305         if (cameraOrder.isEmpty()) {
306             createCameraOrder();
307         }
308         if (++cameraIndex >= cameraOrder.size()) {
309             cameraIndex = 0;
310         }
311         if (motionChangesOrder) {
312             cameraIndex = checkForMotion(cameraIndex);
313         }
314         if (hlsTurnedOn) {
315             discontinuitySequence++;
316             createPlayList();
317         }
318     }
319
320     @Override
321     public void handleCommand(ChannelUID channelUID, Command command) {
322         if (!(command instanceof RefreshType)) {
323             switch (channelUID.getId()) {
324                 case CHANNEL_START_STREAM:
325                     if (OnOffType.ON.equals(command)) {
326                         hlsTurnedOn = true;
327                         for (IpCameraHandler handler : cameraOrder) {
328                             String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
329                                     + handler.getThing().getUID().getId() + ":";
330                             handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
331                         }
332                     } else {
333                         // Do we turn all controls OFF, or do we remember the state before we turned them all on?
334                         hlsTurnedOn = false;
335                     }
336             }
337         }
338     }
339
340     @Override
341     public void initialize() {
342         groupConfig = getConfigAs(GroupConfig.class);
343         serverPort = groupConfig.getServerPort();
344         pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
345         pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
346         motionChangesOrder = groupConfig.getMotionChangesOrder();
347
348         if (serverPort == -1) {
349             logger.warn("The serverPort = -1 which disables a lot of features. See readme for more info.");
350         } else if (serverPort < 1025) {
351             logger.warn("The serverPort is <= 1024 and may cause permission errors under Linux, try a higher port.");
352         }
353         if (groupConfig.getServerPort() > 0) {
354             startStreamServer(true);
355         }
356         updateStatus(ThingStatus.ONLINE);
357         pollCameraGroupJob = pollCameraGroup.scheduleAtFixedRate(this::pollCameraGroup, 10000,
358                 groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
359     }
360
361     @Override
362     public void dispose() {
363         startStreamServer(false);
364         groupTracker.listOfGroupHandlers.remove(this);
365         Future<?> future = pollCameraGroupJob;
366         if (future != null) {
367             future.cancel(true);
368         }
369         cameraOrder.clear();
370     }
371 }