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