]> git.basschouten.com Git - openhab-addons.git/blob
2c409827324e55b995ed2f1f60317aadef8e29be
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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
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 && !getIsAlive()) {
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                 Process localProcess = process;
127                 if (localProcess != null) {
128                     InputStream errorStream = localProcess.getErrorStream();
129                     InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
130                     BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
131                     String line = null;
132                     while ((line = bufferedReader.readLine()) != null) {
133                         logger.debug("{}", line);
134                         if (format.equals(FFmpegFormat.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 will take 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                         }
174                     }
175                 }
176             } catch (IOException e) {
177                 logger.warn("An IO error occured trying to start FFmpeg:{}", e.getMessage());
178             } finally {
179                 switch (format) {
180                     case GIF:
181                         threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
182                         break;
183                     case RECORD:
184                         threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
185                         break;
186                     default:
187                         break;
188                 }
189             }
190         }
191     }
192
193     public void startConverting() {
194         if (!ipCameraFfmpegThread.isAlive()) {
195             ipCameraFfmpegThread = new IpCameraFfmpegThread();
196             if (!password.isEmpty()) {
197                 logger.debug("Starting ffmpeg with this command now:{}",
198                         ffmpegCommand.replaceAll(password, "********"));
199             } else {
200                 logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
201             }
202             ipCameraFfmpegThread.start();
203             if (format.equals(FFmpegFormat.HLS)) {
204                 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
205             }
206         }
207         if (keepAlive != -1) {
208             keepAlive = 8;
209         }
210     }
211
212     public boolean getIsAlive() {
213         Process localProcess = process;
214         if (localProcess != null) {
215             return localProcess.isAlive();
216         }
217         return false;
218     }
219
220     public void stopConverting() {
221         if (ipCameraFfmpegThread.isAlive()) {
222             logger.debug("Stopping ffmpeg {} now when keepalive is:{}", format, keepAlive);
223             Process localProcess = process;
224             if (localProcess != null) {
225                 localProcess.destroyForcibly();
226                 process = null;
227             }
228             if (format.equals(FFmpegFormat.HLS)) {
229                 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);
230             }
231         }
232     }
233 }