[ipcamera] Improve ONVIF preset naming ()

* Refactor to prevent endless loop.
* Allow `-rtsp_transport tcp` to be over-ridden.
* Display actual preset names
* Allow IP to not match due to Hostname given in setup.
* Fix index off by 1
* Bug fixes for HLS
* Compatibility fix for GotoPreset
* Improve default snapshot quality and allow FFmpeg arguments to be
changed.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Connor Petty <mistercpp2000@gmail.com>
This commit is contained in:
Matthew Skinner 2020-11-21 17:27:49 +11:00 committed by GitHub
parent ae7d5715ee
commit 8b3c633b8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 295 additions and 260 deletions

@ -196,6 +196,7 @@ If you do not specify any of these, the binding will use the default which shoul
| `hlsOutOptions`| This gives you direct access to specify your own FFmpeg options to be used. Default: `-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 2 -hls_list_size 4` |
| `gifOutOptions`| This gives you direct access to specify your own FFmpeg options to be used for animated GIF files. Default: `-r 2 -filter_complex scale=-2:360:flags=lanczos,setpts=0.5*PTS,split[o1][o2];[o1]palettegen[p];[o2]fifo[o3];[o3][p]paletteuse` |
| `mjpegOptions` | Allows you to change the settings for creating a MJPEG stream from RTSP using FFmpeg. Possible reasons to change this would be to rotate or re-scale the picture from the camera, change the JPG compression for better quality or the FPS rate. |
| `snapshotOptions` | Specify your own FFmpeg options to be used when creating snapshots from RTSP. Default: `-an -vsync vfr -q:v 2 -update 1` |
| `motionOptions` | This gives access to the FFmpeg parameters for detecting motion alarms from a RTSP stream. One possible use for this is to use the CROP feature to ignore any trees that move in the wind or a timecode stamp. Crop will not remove the trees from your picture, it only ignores the movement of the tree. |
| `gifPreroll`| Store this many snapshots from BEFORE you trigger a GIF creation. Default: `0` will not use snapshots and will instead use a realtime stream from the ffmpegInput URL |
| `ipWhitelist`| Enter any IPs inside brackets that you wish to allow to access the video stream. `DISABLE` the default value will turn this feature off. Example: `ipWhitelist="(127.0.0.1)(192.168.0.99)"` |

@ -19,7 +19,6 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -206,19 +205,6 @@ public class AmcrestHandler extends ChannelDuplexHandler {
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -45,6 +45,7 @@ public class CameraConfig {
private String gifOutOptions = "";
private String mp4OutOptions = "";
private String mjpegOptions = "";
private String snapshotOptions = "";
private String motionOptions = "";
private boolean ptzContinuous;
private int gifPreroll;
@ -61,6 +62,10 @@ public class CameraConfig {
return mjpegOptions;
}
public String getSnapshotOptions() {
return snapshotOptions;
}
public String getMotionOptions() {
return motionOptions;
}

@ -19,7 +19,6 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -252,18 +251,6 @@ public class DahuaHandler extends ChannelDuplexHandler {
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString()) / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -19,9 +19,7 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.ThingHandler;
@ -99,19 +97,6 @@ public class DoorBirdHandler extends ChannelDuplexHandler {
ipCameraHandler.sendHttpGET("/bha-api/light-on.cgi");
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -75,23 +75,20 @@ public class Ffmpeg {
commandArrayList.add(0, ffmpegLocation);
}
public void setKeepAlive(int seconds) {
if (seconds == -1) {
keepAlive = -1;
} else {// We now poll every 8 seconds due to mjpeg stream requirement.
keepAlive = 8; // 64 seconds approx.
public void setKeepAlive(int numberOfEightSeconds) {
// We poll every 8 seconds due to mjpeg stream requirement.
if (keepAlive == -1 && numberOfEightSeconds > 1) {
return;// When set to -1 this will not auto turn off stream.
}
keepAlive = numberOfEightSeconds;
}
public void checkKeepAlive() {
if (keepAlive <= -1) {
return;
} else if (keepAlive == 0) {
} else if (--keepAlive == 0) {
stopConverting();
} else {
keepAlive--;
}
return;
}
private class IpCameraFfmpegThread extends Thread {
@ -119,8 +116,9 @@ public class Ffmpeg {
public void run() {
try {
process = Runtime.getRuntime().exec(commandArrayList.toArray(new String[commandArrayList.size()]));
if (process != null) {
InputStream errorStream = process.getErrorStream();
Process localProcess = process;
if (localProcess != null) {
InputStream errorStream = localProcess.getErrorStream();
InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
String line = null;
@ -189,10 +187,11 @@ public class Ffmpeg {
public void stopConverting() {
if (ipCameraFfmpegThread.isAlive()) {
logger.debug("Stopping ffmpeg {} now", format);
running = false;
if (process != null) {
process.destroyForcibly();
logger.debug("Stopping ffmpeg {} now when keepalive is:{}", format, keepAlive);
Process localProcess = process;
if (localProcess != null) {
localProcess.destroyForcibly();
running = false;
}
if (format.equals(FFmpegFormat.HLS)) {
if (keepAlive == -1) {

@ -19,7 +19,6 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -215,19 +214,6 @@ public class FoscamHandler extends ChannelDuplexHandler {
+ username + "&pwd=" + password);
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -20,9 +20,7 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
@ -441,19 +439,6 @@ public class HikvisionHandler extends ChannelDuplexHandler {
"<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -19,9 +19,7 @@ import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
@ -200,19 +198,6 @@ public class InstarHandler extends ChannelDuplexHandler {
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setioattr&-io_enable=0");
}
return;
case CHANNEL_FFMPEG_MOTION_CONTROL:
if (OnOffType.ON.equals(command)) {
ipCameraHandler.motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
ipCameraHandler.motionAlarmEnabled = false;
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
} else {
ipCameraHandler.motionAlarmEnabled = true;
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
}
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
}
}

@ -45,15 +45,14 @@ public class IpCameraActions implements ThingActions {
return handler;
}
@RuleAction(label = "record an MP4", description = "Record MP4 to a set filename if given, or if filename is null to ipcamera.mp4")
@RuleAction(label = "record a MP4", description = "Record MP4 to a set filename if given, or if filename is null to ipcamera.mp4")
public void recordMP4(
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
logger.debug("Recording {}.mp4 for {} seconds.", filename, secondsToRecord);
if (filename == null && handler != null) {
handler.recordMp4("ipcamera", secondsToRecord);
} else if (handler != null && filename != null) {
handler.recordMp4(filename, secondsToRecord);
IpCameraHandler localHandler = handler;
if (localHandler != null) {
localHandler.recordMp4(filename != null ? filename : "ipcamera", secondsToRecord);
}
}
@ -66,10 +65,9 @@ public class IpCameraActions implements ThingActions {
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
logger.debug("Recording {}.gif for {} seconds.", filename, secondsToRecord);
if (filename == null && handler != null) {
handler.recordGif("ipcamera", secondsToRecord);
} else if (handler != null && filename != null) {
handler.recordGif(filename, secondsToRecord);
IpCameraHandler localHandler = handler;
if (localHandler != null) {
localHandler.recordGif(filename != null ? filename : "ipcamera", secondsToRecord);
}
}

@ -71,37 +71,7 @@ public class IpCameraBindingConstants {
// List of all Thing Config items
public static final String CONFIG_IPADDRESS = "ipAddress";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_ONVIF_PORT = "onvifPort";
public static final String CONFIG_SERVER_PORT = "serverPort";
public static final String CONFIG_USERNAME = "username";
public static final String CONFIG_PASSWORD = "password";
public static final String CONFIG_ONVIF_PROFILE_NUMBER = "onvifMediaProfile";
public static final String CONFIG_POLL_TIME = "pollTime";
public static final String CONFIG_FFMPEG_INPUT = "ffmpegInput";
public static final String CONFIG_SNAPSHOT_URL_OVERRIDE = "snapshotUrl";
public static final String CONFIG_MJPEG_URL = "mjpegUrl";
public static final String CONFIG_FFMPEG_MOTION_INPUT = "alarmInputUrl";
public static final String CONFIG_MOTION_URL_OVERRIDE = "customMotionAlarmUrl";
public static final String CONFIG_AUDIO_URL_OVERRIDE = "customAudioAlarmUrl";
public static final String CONFIG_IMAGE_UPDATE_WHEN = "updateImageWhen";
public static final String CONFIG_NVR_CHANNEL = "nvrChannel";
public static final String CONFIG_IP_WHITELIST = "ipWhitelist";
public static final String CONFIG_FFMPEG_LOCATION = "ffmpegLocation";
public static final String CONFIG_FFMPEG_OUTPUT = "ffmpegOutput";
public static final String CONFIG_FFMPEG_HLS_OUT_ARGUMENTS = "hlsOutOptions";
public static final String CONFIG_FFMPEG_GIF_OUT_ARGUMENTS = "gifOutOptions";
public static final String CONFIG_FFMPEG_MP4_OUT_ARGUMENTS = "mp4OutOptions";
public static final String CONFIG_FFMPEG_MJPEG_ARGUMENTS = "mjpegOptions";
public static final String CONFIG_FFMPEG_MOTION_ARGUMENTS = "motionOptions";
public static final String CONFIG_PTZ_CONTINUOUS = "ptzContinuous";
public static final String CONFIG_GIF_PREROLL = "gifPreroll";
// group thing configs
public static final String CONFIG_FIRST_CAM = "firstCamera";
public static final String CONFIG_SECOND_CAM = "secondCamera";
public static final String CONFIG_THIRD_CAM = "thirdCamera";
public static final String CONFIG_FORTH_CAM = "forthCamera";
public static final String CONFIG_MOTION_CHANGES_ORDER = "motionChangesOrder";
// List of all Channel ids
public static final String CHANNEL_POLL_IMAGE = "pollImage";

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.ipcamera.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link IpCameraDynamicStateDescriptionProvider} Allows the dynamic updating of the ONVIF
* preset names and tokens that can change at any time.
*
* @author Matthew Skinner - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, IpCameraDynamicStateDescriptionProvider.class })
@NonNullByDefault
public class IpCameraDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public IpCameraDynamicStateDescriptionProvider(
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

@ -40,10 +40,13 @@ import org.osgi.service.component.annotations.Reference;
public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
private final @Nullable String openhabIpAddress;
private final GroupTracker groupTracker = new GroupTracker();
private final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
@Activate
public IpCameraHandlerFactory(final @Reference NetworkAddressService networkAddressService) {
public IpCameraHandlerFactory(final @Reference NetworkAddressService networkAddressService,
final @Reference IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
openhabIpAddress = networkAddressService.getPrimaryIpv4HostAddress();
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
@ -59,7 +62,7 @@ public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new IpCameraHandler(thing, openhabIpAddress, groupTracker);
return new IpCameraHandler(thing, openhabIpAddress, groupTracker, stateDescriptionProvider);
} else if (GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new IpCameraGroupHandler(thing, openhabIpAddress, groupTracker);
}

@ -17,6 +17,7 @@ import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -96,20 +97,19 @@ public class StreamServerHandler extends ChannelInboundHandlerAdapter {
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
switch (queryStringDecoder.path()) {
case "/ipcamera.m3u8":
if (ipCameraHandler.ffmpegHLS != null) {
if (!ipCameraHandler.ffmpegHLS.getIsAlive()) {
if (ipCameraHandler.ffmpegHLS != null) {
ipCameraHandler.ffmpegHLS.startConverting();
}
}
} else {
Ffmpeg localFfmpeg = ipCameraHandler.ffmpegHLS;
if (localFfmpeg == null) {
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
} else if (!localFfmpeg.getIsAlive()) {
localFfmpeg.startConverting();
} else {
localFfmpeg.setKeepAlive(8);
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
return;
}
if (ipCameraHandler.ffmpegHLS != null) {
ipCameraHandler.ffmpegHLS.setKeepAlive(8);
}
// Allow files to be created, or you get old m3u8 from the last time this ran.
TimeUnit.MILLISECONDS.sleep(4500);
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
ctx.close();
return;
case "/ipcamera.mpd":
sendFile(ctx, httpRequest.uri(), "application/dash+xml");

@ -23,6 +23,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -361,9 +362,9 @@ public class IpCameraGroupHandler extends BaseThingHandler {
public void dispose() {
startStreamServer(false);
groupTracker.listOfGroupHandlers.remove(this);
if (pollCameraGroupJob != null) {
pollCameraGroupJob.cancel(true);
pollCameraGroupJob = null;
Future<?> future = pollCameraGroupJob;
if (future != null) {
future.cancel(true);
}
cameraOrder.clear();
}

@ -32,6 +32,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -53,6 +54,7 @@ import org.openhab.binding.ipcamera.internal.HttpOnlyHandler;
import org.openhab.binding.ipcamera.internal.InstarHandler;
import org.openhab.binding.ipcamera.internal.IpCameraActions;
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
import org.openhab.binding.ipcamera.internal.IpCameraDynamicStateDescriptionProvider;
import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler;
import org.openhab.binding.ipcamera.internal.StreamServerHandler;
import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection;
@ -125,9 +127,10 @@ import io.netty.util.concurrent.GlobalEventExecutor;
@NonNullByDefault
public class IpCameraHandler extends BaseThingHandler {
public final Logger logger = LoggerFactory.getLogger(getClass());
public final IpCameraDynamicStateDescriptionProvider stateDescriptionProvider;
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4);
private GroupTracker groupTracker;
public CameraConfig cameraConfig;
public CameraConfig cameraConfig = new CameraConfig();
// ChannelGroup is thread safe
public final ChannelGroup mjpegChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@ -391,9 +394,10 @@ public class IpCameraHandler extends BaseThingHandler {
}
}
public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker) {
public IpCameraHandler(Thing thing, @Nullable String ipAddress, GroupTracker groupTracker,
IpCameraDynamicStateDescriptionProvider stateDescriptionProvider) {
super(thing);
cameraConfig = getConfigAs(CameraConfig.class);
this.stateDescriptionProvider = stateDescriptionProvider;
if (ipAddress != null) {
hostIp = ipAddress;
} else {
@ -759,16 +763,13 @@ public class IpCameraHandler extends BaseThingHandler {
mjpegChannelGroup.remove(ctx.channel());
if (mjpegChannelGroup.isEmpty()) {
logger.debug("All ipcamera.mjpeg streams have stopped.");
if (mjpegUri.equals("ffmpeg")) {
if (ffmpegMjpeg != null) {
ffmpegMjpeg.stopConverting();
if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) {
Ffmpeg localMjpeg = ffmpegMjpeg;
if (localMjpeg != null) {
localMjpeg.stopConverting();
}
} else if (!mjpegUri.isEmpty()) {
closeChannel(getTinyUrl(mjpegUri));
} else {
if (ffmpegMjpeg != null) {
ffmpegMjpeg.stopConverting();
}
closeChannel(getTinyUrl(mjpegUri));
}
}
}
@ -887,8 +888,6 @@ public class IpCameraHandler extends BaseThingHandler {
if (rtspUri.toLowerCase().contains("rtsp")) {
if (inputOptions.isEmpty()) {
inputOptions = "-rtsp_transport tcp";
} else {
inputOptions = inputOptions + " -rtsp_transport tcp";
}
}
@ -909,8 +908,9 @@ public class IpCameraHandler extends BaseThingHandler {
cameraConfig.getPassword());
}
}
if (ffmpegHLS != null) {
ffmpegHLS.startConverting();
Ffmpeg localHLS = ffmpegHLS;
if (localHLS != null) {
localHLS.startConverting();
}
break;
case GIF:
@ -934,8 +934,9 @@ public class IpCameraHandler extends BaseThingHandler {
if (cameraConfig.getGifPreroll() > 0) {
storeSnapshots();
}
if (ffmpegGIF != null) {
ffmpegGIF.startConverting();
Ffmpeg localGIF = ffmpegGIF;
if (localGIF != null) {
localGIF.startConverting();
if (gifHistory.isEmpty()) {
gifHistory = gifFilename;
} else if (!gifFilename.equals("ipcamera")) {
@ -957,21 +958,25 @@ public class IpCameraHandler extends BaseThingHandler {
ffmpegRecord = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
cameraConfig.getMp4OutOptions(), cameraConfig.getFfmpegOutput() + mp4Filename + ".mp4",
cameraConfig.getUser(), cameraConfig.getPassword());
ffmpegRecord.startConverting();
if (mp4History.isEmpty()) {
mp4History = mp4Filename;
} else if (!mp4Filename.equals("ipcamera")) {
mp4History = mp4Filename + "," + mp4History;
if (mp4HistoryLength > 49) {
int endIndex = mp4History.lastIndexOf(",");
mp4History = mp4History.substring(0, endIndex);
Ffmpeg localRecord = ffmpegRecord;
if (localRecord != null) {
localRecord.startConverting();
if (mp4History.isEmpty()) {
mp4History = mp4Filename;
} else if (!mp4Filename.equals("ipcamera")) {
mp4History = mp4Filename + "," + mp4History;
if (mp4HistoryLength > 49) {
int endIndex = mp4History.lastIndexOf(",");
mp4History = mp4History.substring(0, endIndex);
}
}
}
setChannelState(CHANNEL_MP4_HISTORY, new StringType(mp4History));
break;
case RTSP_ALARMS:
if (ffmpegRtspHelper != null) {
ffmpegRtspHelper.stopConverting();
Ffmpeg localAlarms = ffmpegRtspHelper;
if (localAlarms != null) {
localAlarms.stopConverting();
if (!audioAlarmEnabled && !motionAlarmEnabled) {
return;
}
@ -996,40 +1001,45 @@ public class IpCameraHandler extends BaseThingHandler {
ffmpegRtspHelper = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, input,
filterOptions + cameraConfig.getMotionOptions(), outputOptions, cameraConfig.getUser(),
cameraConfig.getPassword());
ffmpegRtspHelper.startConverting();
localAlarms = ffmpegRtspHelper;
if (localAlarms != null) {
localAlarms.startConverting();
}
break;
case MJPEG:
if (ffmpegMjpeg == null) {
if (inputOptions.isEmpty()) {
inputOptions = "-hide_banner -loglevel warning";
} else {
inputOptions = inputOptions + " -hide_banner -loglevel warning";
inputOptions += " -hide_banner -loglevel warning";
}
ffmpegMjpeg = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
cameraConfig.getMjpegOptions(),
"http://127.0.0.1:" + cameraConfig.getServerPort() + "/ipcamera.jpg",
cameraConfig.getUser(), cameraConfig.getPassword());
}
if (ffmpegMjpeg != null) {
ffmpegMjpeg.startConverting();
Ffmpeg localMjpeg = ffmpegMjpeg;
if (localMjpeg != null) {
localMjpeg.startConverting();
}
break;
case SNAPSHOT:
// if mjpeg stream you can use ffmpeg -i input.h264 -codec:v copy -bsf:v mjpeg2jpeg output%03d.jpg
// if mjpeg stream you can use 'ffmpeg -i input -codec:v copy -bsf:v mjpeg2jpeg output.jpg'
if (ffmpegSnapshot == null) {
if (inputOptions.isEmpty()) {
// iFrames only
inputOptions = "-threads 1 -skip_frame nokey -hide_banner -loglevel warning";
} else {
inputOptions = inputOptions + " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
inputOptions += " -threads 1 -skip_frame nokey -hide_banner -loglevel warning";
}
ffmpegSnapshot = new Ffmpeg(this, format, cameraConfig.getFfmpegLocation(), inputOptions, rtspUri,
"-an -vsync vfr -update 1",
cameraConfig.getSnapshotOptions(),
"http://127.0.0.1:" + cameraConfig.getServerPort() + "/snapshot.jpg",
cameraConfig.getUser(), cameraConfig.getPassword());
}
if (ffmpegSnapshot != null) {
ffmpegSnapshot.startConverting();
Ffmpeg localSnaps = ffmpegSnapshot;
if (localSnaps != null) {
localSnaps.startConverting();
}
break;
}
@ -1185,7 +1195,7 @@ public class IpCameraHandler extends BaseThingHandler {
motionAlarmEnabled = true;
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
motionAlarmEnabled = false;
noMotionDetected(CHANNEL_MOTION_ALARM);
noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
} else {
motionAlarmEnabled = true;
motionThreshold = Double.valueOf(command.toString());
@ -1194,14 +1204,22 @@ public class IpCameraHandler extends BaseThingHandler {
setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
return;
case CHANNEL_START_STREAM:
Ffmpeg localHLS;
if (OnOffType.ON.equals(command)) {
setupFfmpegFormat(FFmpegFormat.HLS);
if (ffmpegHLS != null) {
ffmpegHLS.setKeepAlive(-1);// will keep running till manually stopped.
localHLS = ffmpegHLS;
if (localHLS == null) {
setupFfmpegFormat(FFmpegFormat.HLS);
localHLS = ffmpegHLS;
}
if (localHLS != null) {
localHLS.setKeepAlive(-1);// Now will run till manually stopped.
localHLS.startConverting();
}
} else {
if (ffmpegHLS != null) {
ffmpegHLS.setKeepAlive(1);
localHLS = ffmpegHLS;
if (localHLS != null) {
// Still runs but will be able to auto stop when the HLS stream is no longer used.
localHLS.setKeepAlive(1);
}
}
return;
@ -1228,8 +1246,9 @@ public class IpCameraHandler extends BaseThingHandler {
sendHttpGET(snapshotUri);// Allows this to change Image FPS on demand
}
} else {
if (ffmpegSnapshot != null) {
ffmpegSnapshot.stopConverting();
Ffmpeg localSnaps = ffmpegSnapshot;
if (localSnaps != null) {
localSnaps.stopConverting();
ffmpegSnapshotGeneration = false;
}
updateImageChannel = false;
@ -1376,8 +1395,9 @@ public class IpCameraHandler extends BaseThingHandler {
updateStatus(ThingStatus.ONLINE);
groupTracker.listOfOnlineCameraHandlers.add(this);
groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
if (cameraConnectionJob != null) {
cameraConnectionJob.cancel(false);
Future<?> localFuture = cameraConnectionJob;
if (localFuture != null) {
localFuture.cancel(false);
}
if (cameraConfig.getGifPreroll() > 0 || cameraConfig.getUpdateImageWhen().contains("1")) {
@ -1482,16 +1502,19 @@ public class IpCameraHandler extends BaseThingHandler {
}
public void stopSnapshotPolling() {
Future<?> localFuture;
if (!streamingSnapshotMjpeg && cameraConfig.getGifPreroll() == 0
&& !cameraConfig.getUpdateImageWhen().contains("1")) {
snapshotPolling = false;
if (snapshotJob != null) {
snapshotJob.cancel(true);
localFuture = snapshotJob;
if (localFuture != null) {
localFuture.cancel(true);
}
} else if (cameraConfig.getUpdateImageWhen().contains("4")) { // only during Motion Alarms
snapshotPolling = false;
if (snapshotJob != null) {
snapshotJob.cancel(true);
localFuture = snapshotJob;
if (localFuture != null) {
localFuture.cancel(true);
}
}
}
@ -1575,8 +1598,9 @@ public class IpCameraHandler extends BaseThingHandler {
}
break;
}
if (ffmpegHLS != null) {
ffmpegHLS.checkKeepAlive();
Ffmpeg localHLS = ffmpegHLS;
if (localHLS != null) {
localHLS.checkKeepAlive();
}
if (openChannels.size() > 18) {
logger.debug("There are {} open Channels being tracked.", openChannels.size());
@ -1681,17 +1705,17 @@ public class IpCameraHandler extends BaseThingHandler {
isOnline = false;
snapshotPolling = false;
onvifCamera.disconnect();
if (pollCameraJob != null) {
pollCameraJob.cancel(true);
pollCameraJob = null;
Future<?> localFuture = pollCameraJob;
if (localFuture != null) {
localFuture.cancel(true);
}
if (snapshotJob != null) {
snapshotJob.cancel(true);
snapshotJob = null;
localFuture = snapshotJob;
if (localFuture != null) {
localFuture.cancel(true);
}
if (cameraConnectionJob != null) {
cameraConnectionJob.cancel(true);
cameraConnectionJob = null;
localFuture = cameraConnectionJob;
if (localFuture != null) {
localFuture.cancel(true);
}
threadPool.shutdown();
threadPool = Executors.newScheduledThreadPool(4);
@ -1707,29 +1731,29 @@ public class IpCameraHandler extends BaseThingHandler {
stopStreamServer();
openChannels.close();
if (ffmpegHLS != null) {
ffmpegHLS.stopConverting();
ffmpegHLS = null;
Ffmpeg localFfmpeg = ffmpegHLS;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
if (ffmpegRecord != null) {
ffmpegRecord.stopConverting();
ffmpegRecord = null;
localFfmpeg = ffmpegRecord;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
if (ffmpegGIF != null) {
ffmpegGIF.stopConverting();
ffmpegGIF = null;
localFfmpeg = ffmpegGIF;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
if (ffmpegRtspHelper != null) {
ffmpegRtspHelper.stopConverting();
ffmpegRtspHelper = null;
localFfmpeg = ffmpegRtspHelper;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
if (ffmpegMjpeg != null) {
ffmpegMjpeg.stopConverting();
ffmpegMjpeg = null;
localFfmpeg = ffmpegMjpeg;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
if (ffmpegSnapshot != null) {
ffmpegSnapshot.stopConverting();
ffmpegSnapshot = null;
localFfmpeg = ffmpegSnapshot;
if (localFfmpeg != null) {
localFfmpeg.stopConverting();
}
channelTrackingMap.clear();
}

@ -21,9 +21,11 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@ -33,6 +35,8 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ipcamera.internal.Helper;
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -145,8 +149,9 @@ public class OnvifConnection {
private String ptzNodeToken = "000";
private String ptzConfigToken = "000";
private int presetTokenIndex = 0;
private LinkedList<String> presetTokens = new LinkedList<>();
private LinkedList<String> mediaProfileTokens = new LinkedList<>();
private List<String> presetTokens = new LinkedList<>();
private List<String> presetNames = new LinkedList<>();
private List<String> mediaProfileTokens = new LinkedList<>();
private boolean ptzDevice = true;
public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
@ -277,8 +282,7 @@ public class OnvifConnection {
case GotoPreset:
return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
+ presetTokens.get(presetTokenIndex)
+ "</PresetToken><Speed><PanTilt x=\"0.0\" y=\"0.0\" space=\"\"></PanTilt><Zoom x=\"0.0\" space=\"\"></Zoom></Speed></GotoPreset>";
+ presetTokens.get(presetTokenIndex) + "</PresetToken></GotoPreset>";
case GetPresets:
return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
@ -326,7 +330,7 @@ public class OnvifConnection {
} else if (message.contains("GetStatusResponse")) {
processPTZLocation(message);
} else if (message.contains("GetPresetsResponse")) {
presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
parsePresets(message);
} else if (message.contains("GetConfigurationsResponse")) {
sendPTZRequest(RequestType.GetPresets);
ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
@ -357,14 +361,16 @@ public class OnvifConnection {
HttpRequest requestBuilder(RequestType requestType, String xAddr) {
logger.trace("Sending ONVIF request:{}", requestType);
String security = "";
String extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
String extraEnvelope = "";
String headerTo = "";
String getXmlCache = getXml(requestType);
if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
|| requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
headerTo = "<a:To s:mustUnderstand=\"1\">http://" + ipAddress + xAddr + "</a:To>";
extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
}
if (!password.isEmpty()) {
String headers;
if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
String nonce = createNonce();
String dateTime = getUTCdateTime();
String digest = createDigest(nonce, dateTime);
@ -376,17 +382,15 @@ public class OnvifConnection {
+ encodeBase64(nonce)
+ "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
+ dateTime + "</Created></UsernameToken></Security>";
}
String headers = "<s:Header>" + security + headerTo + "</s:Header>";
if (requestType.equals(RequestType.GetSystemDateAndTime)) {
extraEnvelope = "";
headers = "<s:Header>" + security + headerTo + "</s:Header>";
} else {// GetSystemDateAndTime must not be password protected as per spec.
headers = "";
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"), xAddr);
request.headers().add("Content-Type", "application/soap+xml");
request.headers().add("charset", "utf-8");
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
request.headers().add("Content-Type",
"application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
request.headers().add("Charset", "utf-8");
if (onvifPort != 80) {
request.headers().set("Host", ipAddress + ":" + onvifPort);
} else {
@ -398,7 +402,6 @@ public class OnvifConnection {
+ headers
+ "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ getXmlCache + "</s:Body></s:Envelope>";
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
request.headers().set("Content-Length", bbuf.readableBytes());
@ -408,15 +411,14 @@ public class OnvifConnection {
/**
* The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
* leaving just the
* URL.
* leaving just the URL.
*
* @author Matthew Skinner - Initial contribution
*/
String removeIPfromUrl(String url) {
int index = url.indexOf(ipAddress);
int index = url.indexOf("//");
if (index != -1) {// now remove the :port
index = url.indexOf("/", index + ipAddress.length());
index = url.indexOf("/", index + 2);
}
if (index == -1) {
logger.debug("We hit an issue parsing url:{}", url);
@ -456,11 +458,10 @@ public class OnvifConnection {
String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
logger.debug("Cameras UTC time is : {}:{}:{}", hour, minute, second);
String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
logger.debug("Cameras UTC date is : {}-{}-{}", year, month, day);
logger.debug("Cameras UTC dateTime is:{}-{}-{}T{}:{}:{}", year, month, day, hour, minute, second);
}
private String getUTCdateTime() {
@ -718,8 +719,8 @@ public class OnvifConnection {
this.mediaProfileIndex = mediaProfileIndex;
}
LinkedList<String> listOfResults(String message, String heading, String key) {
LinkedList<String> results = new LinkedList<String>();
List<String> listOfResults(String message, String heading, String key) {
List<String> results = new LinkedList<>();
String temp = "";
for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
@ -728,13 +729,31 @@ public class OnvifConnection {
if (!temp.isEmpty()) {
logger.trace("String was found:{}", temp);
results.add(temp);
++startLookingFromIndex;
} else {
return results;// key string must not exist so stop looking.
}
startLookingFromIndex += temp.length();
}
}
return results;
}
void parsePresets(String message) {
List<StateOption> presets = new ArrayList<>();
int counter = 1;// Presets start at 1 not 0. HOME may be added to index 0.
presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
presetNames = listOfResults(message, "<tptz:Preset", "<tt:Name>");
if (presetTokens.size() != presetNames.size()) {
logger.warn("Camera did not report the same number of Tokens and Names for PTZ presets");
return;
}
for (String value : presetNames) {
presets.add(new StateOption(Integer.toString(counter++), value));
}
ipCameraHandler.stateDescriptionProvider
.setStateOptions(new ChannelUID(ipCameraHandler.getThing().getUID(), CHANNEL_GOTO_PRESET), presets);
}
void parseProfiles(String message) {
mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
if (mediaProfileIndex >= mediaProfileTokens.size()) {

@ -241,6 +241,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -479,6 +487,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -722,6 +738,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -1006,6 +1030,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -1272,6 +1304,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -1535,6 +1575,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -1823,6 +1871,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>
@ -2098,6 +2154,14 @@
<advanced>true</advanced>
</parameter>
<parameter name="snapshotOptions" type="text" required="false" groupName="FFmpeg Setup">
<label>Snapshot Options</label>
<description>Specify your own FFmpeg options to be used when creating snapshots from RTSP.
</description>
<default>-an -vsync vfr -q:v 2 -update 1</default>
<advanced>true</advanced>
</parameter>
<parameter name="alarmInputUrl" type="text" required="false" groupName="FFmpeg Setup">
<context>url</context>
<label>Alarm Input URL</label>