2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
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;
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;
38 * The {@link Ffmpeg} class is responsible for handling multiple ffmpeg conversions which are used for many tasks
41 * @author Matthew Skinner - Initial contribution
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;
56 public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
57 String input, String outArguments, String output, String username, String password) {
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);
68 if (inputArguments.isEmpty()) {
69 ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
71 ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
73 Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
74 // ffmpegLocation may have a space in its folder
75 commandArrayList.add(0, ffmpegLocation);
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.
83 keepAlive = numberOfEightSeconds;
86 public void checkKeepAlive() {
89 } else if (keepAlive <= -1 && !getIsAlive()) {
90 logger.warn("HLS stream was not running, restarting it now.");
98 private class IpCameraFfmpegThread extends Thread {
99 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
100 public int countOfMotions;
102 IpCameraFfmpegThread() {
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));
113 private void mp4Created() {
114 ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
115 ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
116 new DecimalType(++ipCameraHandler.mp4HistoryLength));
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);
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.
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.
150 } else if (line.contains("speed=")) {
151 if (countOfMotions > 0) {
152 if (ipCameraHandler.motionThreshold.intValue() > 89) {
155 if (ipCameraHandler.motionThreshold.intValue() > 10) {
160 if (countOfMotions <= 0) {
161 ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
165 } else if (line.contains("silence_start")) {
166 ipCameraHandler.noAudioDetected();
167 } else if (line.contains("silence_end")) {
168 ipCameraHandler.audioDetected();
173 } catch (IOException e) {
174 logger.warn("An error occured trying to process the messages from FFmpeg.");
178 threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
181 threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
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, "********"));
197 logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
199 ipCameraFfmpegThread.start();
200 if (format.equals(FFmpegFormat.HLS)) {
201 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
204 if (keepAlive != -1) {
209 public boolean getIsAlive() {
210 Process localProcess = process;
211 if (localProcess != null) {
212 return localProcess.isAlive();
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();
225 if (format.equals(FFmpegFormat.HLS)) {
226 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);