]> git.basschouten.com Git - openhab-addons.git/blob
c728d633e20cf4e5b5de1bef2ce93438169b2dfa
[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;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
16
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.net.URLEncoder;
22 import java.nio.charset.StandardCharsets;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.ScheduledExecutorService;
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.IpCameraBindingConstants.FFmpegFormat;
33 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link Ffmpeg} class is responsible for handling multiple ffmpeg conversions which are used for many tasks
41  *
42  *
43  * @author Matthew Skinner - Initial contribution
44  */
45
46 @NonNullByDefault
47 public class Ffmpeg {
48     private final Logger logger = LoggerFactory.getLogger(getClass());
49     private IpCameraHandler ipCameraHandler;
50     private @Nullable Process process = null;
51     private String ffmpegCommand = "";
52     private FFmpegFormat format;
53     private List<String> commandArrayList = new ArrayList<String>();
54     private IpCameraFfmpegThread ipCameraFfmpegThread = new IpCameraFfmpegThread();
55     private int keepAlive = 8;
56     private String password;
57     private Boolean notFrozen = true;
58
59     public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
60             String input, String outArguments, String output, String username, String password) {
61         this.format = format;
62         this.password = URLEncoder.encode(password, StandardCharsets.UTF_8);
63
64         ipCameraHandler = handle;
65         String altInput = input;
66         // Input can be snapshots not just rtsp or http
67         if (!password.isEmpty() && !input.contains("@") && input.contains("rtsp")) {
68             String credentials = username + ":" + this.password + "@";
69             // will not work for https: but currently binding does not use https
70             altInput = input.substring(0, 7) + credentials + input.substring(7);
71         }
72         if (inputArguments.isEmpty()) {
73             ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
74         } else {
75             ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
76         }
77         Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
78         // ffmpegLocation may have a space in its folder
79         commandArrayList.add(0, ffmpegLocation);
80     }
81
82     public void setKeepAlive(int numberOfEightSeconds) {
83         // We poll every 8 seconds due to mjpeg stream requirement.
84         if (keepAlive == -1 && numberOfEightSeconds > 1) {
85             return;// When set to -1 this will not auto turn off stream.
86         }
87         keepAlive = numberOfEightSeconds;
88     }
89
90     public void checkKeepAlive() {
91         if (keepAlive == 1) {
92             stopConverting();
93         } else if (keepAlive <= -1 && !getIsAlive()) {
94             logger.warn("HLS stream was not running, restarting it now.");
95             startConverting();
96         }
97         if (keepAlive > 0) {
98             keepAlive--;
99         }
100     }
101
102     private class IpCameraFfmpegThread extends Thread {
103         private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
104         public int countOfMotions;
105
106         IpCameraFfmpegThread() {
107             setDaemon(true);
108         }
109
110         private void gifCreated() {
111             // Without a small delay, Pushover sends no file 10% of time.
112             ipCameraHandler.setChannelState(CHANNEL_RECORDING_GIF, DecimalType.ZERO);
113             ipCameraHandler.setChannelState(CHANNEL_GIF_HISTORY_LENGTH,
114                     new DecimalType(++ipCameraHandler.gifHistoryLength));
115         }
116
117         private void mp4Created() {
118             ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
119             ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
120                     new DecimalType(++ipCameraHandler.mp4HistoryLength));
121         }
122
123         @Override
124         public void run() {
125             try {
126                 process = Runtime.getRuntime().exec(commandArrayList.toArray(new String[commandArrayList.size()]));
127                 Process localProcess = process;
128                 if (localProcess != null) {
129                     InputStream errorStream = localProcess.getErrorStream();
130                     InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
131                     BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
132                     String line = null;
133                     while ((line = bufferedReader.readLine()) != null) {
134                         logger.trace("{}", line);
135                         switch (format) {
136                             case RTSP_ALARMS:
137                                 if (line.contains("lavfi.")) {
138                                     // When the number of pixels that change are below the noise floor we need to look
139                                     // across frames to confirm it is motion and not noise.
140                                     if (countOfMotions < 10) {// Stop increasing otherwise it takes too long to go OFF
141                                         countOfMotions++;
142                                     }
143                                     if (countOfMotions > 9) {
144                                         ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
145                                     } else if (countOfMotions > 4 && ipCameraHandler.motionThreshold.intValue() > 10) {
146                                         ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
147                                     } else if (countOfMotions > 3 && ipCameraHandler.motionThreshold.intValue() > 15) {
148                                         ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
149                                     } else if (countOfMotions > 2 && ipCameraHandler.motionThreshold.intValue() > 30) {
150                                         ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
151                                     } else if (countOfMotions > 0 && ipCameraHandler.motionThreshold.intValue() > 89) {
152                                         ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
153                                         countOfMotions = 4;// Used to debounce the Alarm.
154                                     }
155                                 } else if (line.contains("speed=")) {
156                                     if (countOfMotions > 0) {
157                                         if (ipCameraHandler.motionThreshold.intValue() > 89) {
158                                             countOfMotions--;
159                                         }
160                                         if (ipCameraHandler.motionThreshold.intValue() > 10) {
161                                             countOfMotions -= 2;
162                                         } else {
163                                             countOfMotions -= 4;
164                                         }
165                                         if (countOfMotions <= 0) {
166                                             ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
167                                             countOfMotions = 0;
168                                         }
169                                     }
170                                 } else if (line.contains("silence_start")) {
171                                     ipCameraHandler.noAudioDetected();
172                                 } else if (line.contains("silence_end")) {
173                                     ipCameraHandler.audioDetected();
174                                 }
175                             case MJPEG:
176                             case SNAPSHOT:
177                                 notFrozen = true;// RTSP_ALARMS, MJPEG and SNAPSHOT all set this to true, no break.
178                                 break;
179                         }
180                     }
181                 }
182             } catch (IOException e) {
183                 logger.warn("An IO error occured trying to start FFmpeg:{}", e.getMessage());
184             } finally {
185                 switch (format) {
186                     case GIF:
187                         threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
188                         break;
189                     case RECORD:
190                         threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
191                         break;
192                     default:
193                         break;
194                 }
195             }
196         }
197     }
198
199     public void startConverting() {
200         if (!ipCameraFfmpegThread.isAlive()) {
201             ipCameraFfmpegThread = new IpCameraFfmpegThread();
202             if (!password.isEmpty()) {
203                 logger.debug("Starting ffmpeg with this command now:{}",
204                         ffmpegCommand.replaceAll(password, "********"));
205             } else {
206                 logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
207             }
208             ipCameraFfmpegThread.start();
209             if (format.equals(FFmpegFormat.HLS)) {
210                 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
211             }
212         }
213         if (keepAlive != -1) {
214             keepAlive = 8;
215         }
216     }
217
218     public boolean getIsAlive() {
219         Process localProcess = process;
220         if (localProcess != null) {
221             if (localProcess.isAlive() && notFrozen) {
222                 notFrozen = false; // Any process output will set this back to true before next check.
223                 return true;
224             }
225         }
226         return false;
227     }
228
229     public void stopConverting() {
230         if (ipCameraFfmpegThread.isAlive()) {
231             logger.debug("Stopping ffmpeg {} now when keepalive is:{}", format, keepAlive);
232             Process localProcess = process;
233             if (localProcess != null) {
234                 localProcess.destroyForcibly();
235                 process = null;
236             }
237             if (format.equals(FFmpegFormat.HLS)) {
238                 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);
239             }
240         }
241     }
242 }