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