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