2 * Copyright (c) 2010-2024 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
42 * @author Matthew Skinner - Initial contribution
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;
58 public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
59 String input, String outArguments, String output, String username, String password) {
61 this.password = URLEncoder.encode(password, StandardCharsets.UTF_8);
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);
71 if (inputArguments.isEmpty()) {
72 ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
74 ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
76 Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
77 // ffmpegLocation may have a space in its folder
78 commandArrayList.add(0, ffmpegLocation);
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.
86 keepAlive = numberOfEightSeconds;
89 public void checkKeepAlive() {
92 } else if (keepAlive <= -1 && !isAlive()) {
93 logger.warn("HLS stream was not running, restarting it now.");
101 private class IpCameraFfmpegThread extends Thread {
102 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
103 public int countOfMotions;
105 IpCameraFfmpegThread() {
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));
116 private void mp4Created() {
117 ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
118 ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
119 new DecimalType(++ipCameraHandler.mp4HistoryLength));
125 process = Runtime.getRuntime().exec(commandArrayList.toArray(new String[commandArrayList.size()]));
127 InputStream errorStream = process.getErrorStream();
128 InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
129 BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
131 while ((line = bufferedReader.readLine()) != null) {
132 logger.trace("{}", line);
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
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.
153 } else if (line.contains("speed=")) {
154 if (countOfMotions > 0) {
155 if (ipCameraHandler.motionThreshold.intValue() > 89) {
158 if (ipCameraHandler.motionThreshold.intValue() > 10) {
163 if (countOfMotions <= 0) {
164 ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
168 } else if (line.contains("silence_start")) {
169 ipCameraHandler.noAudioDetected();
170 } else if (line.contains("silence_end")) {
171 ipCameraHandler.audioDetected();
175 notFrozen = true; // RTSP_ALARMS, MJPEG and SNAPSHOT all set this to true, no break.
181 } catch (IOException e) {
182 logger.warn("An IO error occurred trying to start FFmpeg: {}", e.getMessage());
186 threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
189 threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
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, "********"));
205 logger.debug("Starting ffmpeg with this command now: {}", ffmpegCommand);
207 ipCameraFfmpegThread.start();
208 if (format.equals(FFmpegFormat.HLS)) {
209 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
212 if (keepAlive != -1) {
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.
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();
236 if (format.equals(FFmpegFormat.HLS)) {
237 ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);