2 * Copyright (c) 2010-2023 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.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;
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;
40 * The {@link Ffmpeg} class is responsible for handling multiple ffmpeg conversions which are used for many tasks
43 * @author Matthew Skinner - Initial contribution
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;
59 public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
60 String input, String outArguments, String output, String username, String password) {
62 this.password = URLEncoder.encode(password, StandardCharsets.UTF_8);
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);
72 if (inputArguments.isEmpty()) {
73 ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
75 ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
77 Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
78 // ffmpegLocation may have a space in its folder
79 commandArrayList.add(0, ffmpegLocation);
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.
87 keepAlive = numberOfEightSeconds;
90 public void checkKeepAlive() {
93 } else if (keepAlive <= -1 && !getIsAlive()) {
94 logger.warn("HLS stream was not running, restarting it now.");
102 private class IpCameraFfmpegThread extends Thread {
103 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
104 public int countOfMotions;
106 IpCameraFfmpegThread() {
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));
117 private void mp4Created() {
118 ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
119 ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
120 new DecimalType(++ipCameraHandler.mp4HistoryLength));
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);
133 while ((line = bufferedReader.readLine()) != null) {
134 logger.trace("{}", line);
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
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.
155 } else if (line.contains("speed=")) {
156 if (countOfMotions > 0) {
157 if (ipCameraHandler.motionThreshold.intValue() > 89) {
160 if (ipCameraHandler.motionThreshold.intValue() > 10) {
165 if (countOfMotions <= 0) {
166 ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
170 } else if (line.contains("silence_start")) {
171 ipCameraHandler.noAudioDetected();
172 } else if (line.contains("silence_end")) {
173 ipCameraHandler.audioDetected();
177 notFrozen = true;// RTSP_ALARMS, MJPEG and SNAPSHOT all set this to true, no break.
184 } catch (IOException e) {
185 logger.warn("An IO error occured trying to start FFmpeg:{}", e.getMessage());
189 threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
192 threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
201 public void startConverting() {
202 if (!ipCameraFfmpegThread.isAlive()) {
203 ipCameraFfmpegThread = new IpCameraFfmpegThread();
204 if (!password.isEmpty()) {
205 logger.debug("Starting ffmpeg with this command now:{}",
206 ffmpegCommand.replaceAll(password, "********"));
208 logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
210 ipCameraFfmpegThread.start();
211 if (format.equals(FFmpegFormat.HLS)) {
212 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
215 if (keepAlive != -1) {
220 public boolean getIsAlive() {
221 Process localProcess = process;
222 if (localProcess != null) {
223 if (localProcess.isAlive() && notFrozen) {
224 notFrozen = false; // Any process output will set this back to true before next check.
231 public void stopConverting() {
232 if (ipCameraFfmpegThread.isAlive()) {
233 logger.debug("Stopping ffmpeg {} now when keepalive is:{}", format, keepAlive);
234 Process localProcess = process;
235 if (localProcess != null) {
236 localProcess.destroyForcibly();
239 if (format.equals(FFmpegFormat.HLS)) {
240 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);