diff --git a/sensors/video/sensorhub-driver-axis/build.gradle b/sensors/video/sensorhub-driver-axis/build.gradle index c528da9b9..f727d22ee 100644 --- a/sensors/video/sensorhub-driver-axis/build.gradle +++ b/sensors/video/sensorhub-driver-axis/build.gradle @@ -1,15 +1,11 @@ description = 'Axis Video Camera' ext.details = 'Driver for IP video cameras from Axis (with PTZ tasking support)' -version = '1.0.0' +version = '1.1.0' dependencies { implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion implementation project(':sensorhub-driver-rtpcam') - embeddedImpl('net.sf.jipcam:jipcam:0.9.1') { - exclude group: 'javax.servlet', module: 'servlet-api' - exclude group: 'commons-cli', module: 'commons-cli' - exclude group: 'commons-httpclient', module: 'commons-httpclient' - } + implementation project(':sensorhub-driver-ffmpeg') testImplementation project(path: ':sensorhub-driver-videocam', configuration: 'testArtifacts') } diff --git a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AXISJpegHeaderReader.java b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AXISJpegHeaderReader.java deleted file mode 100644 index b293b3c83..000000000 --- a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AXISJpegHeaderReader.java +++ /dev/null @@ -1,50 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK *************************** - -The contents of this file are subject to the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the License. - -The Initial Developer is Sensia Software LLC. Portions created by the Initial -Developer are Copyright (C) 2014 the Initial Developer. All Rights Reserved. - -******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.axis; - -/** - * TODO implement better header reader - * - * @author Johannes Echterhoff - * - */ -public class AXISJpegHeaderReader -{ - - public static long getTimestampAsDateTime(byte[] axisJpeg) - { - // TODO need to update to better DateTime object o get time zone support - return AXISJpegHeaderReader.getTimestamp(axisJpeg); - } - - public static long getTimestamp(byte[] axisJpeg) - { - - byte timestamp1 = axisJpeg[25]; - byte timestamp2 = axisJpeg[26]; - byte timestamp3 = axisJpeg[27]; - byte timestamp4 = axisJpeg[28]; - byte timestamp5 = axisJpeg[29]; - - long secondsSinceEpoc = (((long) ((int) timestamp1 & 0xFF)) << 24) + (((long) ((int) timestamp2 & 0xFF)) << 16) + (((long) ((int) timestamp3 & 0xFF)) << 8) + ((long) ((int) timestamp4 & 0xFF)); - - long subseconds = (long) ((int) timestamp5 & 0xFF); - - long millisSinceEpoc = (secondsSinceEpoc * 1000) + subseconds; - - return millisSinceEpoc; - } -} diff --git a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraConfig.java b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraConfig.java index 1beb03828..efa980580 100644 --- a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraConfig.java +++ b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraConfig.java @@ -45,7 +45,16 @@ public class AxisCameraConfig extends SensorConfig { @DisplayInfo(label="HTTP", desc="HTTP configuration") public HTTPConfig http = new HTTPConfig(); - @DisplayInfo(label="RTP/RTSP", desc="RTP/RTSP configuration (Remote host is obtained from HTTP configuration)") + /** + * RTP/RTSP configuration (Remote host is obtained from HTTP configuration) + * + *
{@code localUdpPort} of {@link RTSPConfig} is no longer honored by + * this driver. FFmpeg chooses its own client ports during RTSP SETUP (or + * uses TCP interleaved via {@code -rtsp_transport tcp}). The field is + * retained only for backward-compatibility
+ */ + + @DisplayInfo(label="RTP/RTSP", desc="RTP/RTSP configuration") public RTSPConfig rtsp = new RTSPConfig(); @DisplayInfo(label="Connection Options") @@ -62,7 +71,10 @@ public class AxisCameraConfig extends SensorConfig { @DisplayInfo(label="Enable H264", desc="Enable H264 encoded video output (accessible through RTSP)") public boolean enableH264; - + + @DisplayInfo(label="Enable H265", desc="Enable H265 (HEVC) encoded video output (accessible through RTSP)") + public boolean enableH265; + @DisplayInfo(label="Enable MJPEG", desc="Enable MJPEG encoded video output (accessible through HTTP)") public boolean enableMJPEG; diff --git a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraDriver.java b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraDriver.java index 67648e47e..68f395f84 100644 --- a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraDriver.java +++ b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisCameraDriver.java @@ -20,6 +20,8 @@ Developer are Copyright (C) 2014 the Initial Developer. All Rights Reserved. import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import net.opengis.sensorml.v20.IdentifierList; import net.opengis.sensorml.v20.Term; import org.sensorhub.api.sensor.SensorException; @@ -27,7 +29,6 @@ Developer are Copyright (C) 2014 the Initial Developer. All Rights Reserved. import org.sensorhub.impl.module.RobustConnection; import org.sensorhub.impl.security.ClientAuth; import org.sensorhub.impl.sensor.AbstractSensorModule; -import org.sensorhub.impl.sensor.rtpcam.RTPVideoOutput; import org.sensorhub.impl.sensor.rtpcam.RTSPClient; import org.sensorhub.api.common.SensorHubException; import org.vast.sensorML.SMLFactory; @@ -55,8 +56,9 @@ public class AxisCameraDriver extends AbstractSensorModule < AxisCameraConfig > public final String VAPIX_API_BASE_URL = "/axis-cgi"; RobustConnection connection; - AxisVideoOutput mjpegVideoOutput; - RTPVideoOutput < AxisCameraDriver > h264VideoOutput; + AxisVideoStream mjpegVideoStream; + AxisVideoStream h264VideoStream; + AxisVideoStream h265VideoStream; AxisPtzOutput ptzPosOutput; AxisVideoControl videoControlInterface; AxisPtzControl ptzControlInterface; @@ -72,6 +74,7 @@ public class AxisCameraDriver extends AbstractSensorModule < AxisCameraConfig > boolean ptzSupported = false; boolean mjpegSupported = false; boolean h264Supported = false; + boolean h265Supported = false; boolean mpeg4Supported = false; public AxisCameraDriver() { @@ -102,8 +105,9 @@ public void setConfiguration(final AxisCameraConfig config) { protected void doInit() throws SensorHubException { // reset internal state in case init() was already called super.doInit(); - mjpegVideoOutput = null; - h264VideoOutput = null; + mjpegVideoStream = null; + h264VideoStream = null; + h265VideoStream = null; ptzPosOutput = null; ptzControlInterface = null; ptzSupported = false; @@ -132,9 +136,12 @@ else if (tokens[0].trim().equalsIgnoreCase("root.Properties.PTZ.PTZ")) { if (tokens[1].trim().equalsIgnoreCase("yes")) ptzSupported = true; } else if (tokens[0].trim().equalsIgnoreCase("root.Properties.Image.Format")) { - mpeg4Supported = tokens[1].toLowerCase().contains("mpeg4"); - h264Supported = tokens[1].toLowerCase().contains("h264"); - mjpegSupported = tokens[1].toLowerCase().contains("mjpeg"); + String formats = tokens[1].toLowerCase(); + mpeg4Supported = formats.contains("mpeg4"); + h264Supported = formats.contains("h264"); + // Axis VAPIX reports HEVC as either "h265" or "hevc" depending on firmware. + h265Supported = formats.contains("h265") || formats.contains("hevc"); + mjpegSupported = formats.contains("mjpeg"); } else if (tokens[0].trim().equalsIgnoreCase("root.Brand.ProdFullName")) longName = tokens[1]; else if (tokens[0].trim().equalsIgnoreCase("root.Brand.ProdShortName")) @@ -162,11 +169,15 @@ else if (tokens[0].trim().equalsIgnoreCase("root.Properties.API.HTTP.Version")) if (!h264Supported && config.enableH264) throw new IOException("Cannot connect to RTSP server - H264 not supported"); + if (!h265Supported && config.enableH265) + throw new IOException("Cannot connect to RTSP server - H265 not supported"); + if (vapixVersion != 2 && vapixVersion != 3) throw new IOException("Unsupported VAPIX API for this camera. VAPIX API version returned: " + vapixVersion); - // check connection to RTSP server - if (h264Supported && config.enableH264) { + // check connection to RTSP server (used for both H.264 and H.265 paths) + boolean rtspWanted = (h264Supported && config.enableH264) || (h265Supported && config.enableH265); + if (rtspWanted) { try { RTSPClient rtspClient = new RTSPClient( @@ -206,20 +217,28 @@ else if (tokens[0].trim().equalsIgnoreCase("root.Properties.API.HTTP.Version")) throw new SensorException("Cannot connect to MJPEG stream - MJPEG not supported"); } - // add MJPEG video output + // add MJPEG video output (HTTP via FFmpeg) if (mjpegSupported && config.enableMJPEG) { String outputName = videoOutName + videoOutNum++; - mjpegVideoOutput = new AxisVideoOutput(this, outputName); - mjpegVideoOutput.init(); - addOutput(mjpegVideoOutput, false); + mjpegVideoStream = new AxisVideoStream(this, outputName, buildMjpegUrl(), "-timeout 3000000"); + mjpegVideoStream.init(); + addOutput(mjpegVideoStream.getOutput(), false); } - // add H264 video output + // add H.264 video output (RTSP via FFmpeg) if (h264Supported && config.enableH264) { String outputName = videoOutName + videoOutNum++; - h264VideoOutput = new RTPVideoOutput<>(outputName, this); - h264VideoOutput.init(config.video.resolution.getWidth(), config.video.resolution.getHeight()); - addOutput(h264VideoOutput, false); + h264VideoStream = new AxisVideoStream(this, outputName, buildRtspUrl("h264"), "-rtsp_transport tcp -stimeout 3000000"); + h264VideoStream.init(); + addOutput(h264VideoStream.getOutput(), false); + } + + // add H.265 video output (RTSP via FFmpeg) + if (h265Supported && config.enableH265) { + String outputName = videoOutName + videoOutNum++; + h265VideoStream = new AxisVideoStream(this, outputName, buildRtspUrl("h265"), "-rtsp_transport tcp -stimeout 3000000"); + h265VideoStream.init(); + addOutput(h265VideoStream.getOutput(), false); } if (ptzSupported) { @@ -241,12 +260,15 @@ protected void doStart() throws SensorHubException { // wait for valid connection to camera connection.waitForConnection(); - // start video output - if (mjpegVideoOutput != null) - mjpegVideoOutput.start(); + // start video outputs + if (mjpegVideoStream != null) + mjpegVideoStream.start(); - if (h264VideoOutput != null) - h264VideoOutput.start(config.video, config.rtsp, config.connection.connectTimeout); + if (h264VideoStream != null) + h264VideoStream.start(); + + if (h265VideoStream != null) + h265VideoStream.start(); // if PTZ supported if (ptzSupported) { @@ -340,11 +362,14 @@ protected void doStop() { if (ptzControlInterface != null) ptzControlInterface.stop(); - if (mjpegVideoOutput != null) - mjpegVideoOutput.stop(); + if (mjpegVideoStream != null) + mjpegVideoStream.stop(); + + if (h264VideoStream != null) + h264VideoStream.stop(); - if (h264VideoOutput != null) - h264VideoOutput.stop(); + if (h265VideoStream != null) + h265VideoStream.stop(); if (videoControlInterface != null) videoControlInterface.stop(); @@ -359,4 +384,53 @@ protected String getHostUrl() { setAuth(); return hostUrl; } + + + /** + * Builds the FFmpeg-compatible MJPEG-over-HTTP URL, inlining credentials + * if the user/password are configured (FFmpeg cannot consult + * {@link ClientAuth} the way the Java {@code URL} opener can). + */ + protected String buildMjpegUrl() { + String userInfo = buildUserInfo(config.http.user, config.http.password); + return "http://" + userInfo + config.http.remoteHost + ":" + config.http.remotePort + "/mjpg/video.mjpg"; + } + + + /** + * Builds the FFmpeg-compatible RTSP URL for the requested codec. The base + * path is taken from {@code config.rtsp.videoPath}; the {@code videocodec} + * query parameter is rewritten to the requested codec so the same + * configuration field can serve both H.264 and H.265 streams. + * + * @param codec one of {@code "h264"}, {@code "h265"}, or {@code "jpeg"}. + */ + protected String buildRtspUrl(String codec) { + String userInfo = buildUserInfo(config.rtsp.user, config.rtsp.password); + String path = config.rtsp.videoPath == null ? DEFAULT_RTSP_VIDEO_PATH : config.rtsp.videoPath; + if (path.toLowerCase().contains("videocodec=")) { + path = path.replaceAll("(?i)videocodec=[a-z0-9]+", "videocodec=" + codec); + } else { + path = path + (path.contains("?") ? "&" : "?") + "videocodec=" + codec; + } + if (!path.startsWith("/")) + path = "/" + path; + return "rtsp://" + userInfo + config.rtsp.remoteHost + ":" + config.rtsp.remotePort + path; + } + + + /** + * Returns a URL userinfo segment ({@code "user:pass@"}) with both components + * URL-encoded, or the empty string if no credentials are configured. + */ + private static String buildUserInfo(String user, String password) { + if (user == null || user.isEmpty()) + return ""; + StringBuilder sb = new StringBuilder(); + sb.append(URLEncoder.encode(user, StandardCharsets.UTF_8)); + if (password != null && !password.isEmpty()) + sb.append(':').append(URLEncoder.encode(password, StandardCharsets.UTF_8)); + sb.append('@'); + return sb.toString(); + } } \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoOutput.java b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoOutput.java deleted file mode 100644 index 26607d563..000000000 --- a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoOutput.java +++ /dev/null @@ -1,199 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK *************************** - -The contents of this file are subject to the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the License. - -The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial -Developer are Copyright (C) 2014 the Initial Developer. All Rights Reserved. - -******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.axis; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.MalformedURLException; -import javax.media.Buffer; -import net.opengis.swe.v20.DataBlock; -import net.opengis.swe.v20.DataComponent; -import net.opengis.swe.v20.DataEncoding; -import net.opengis.swe.v20.DataStream; -import net.sf.jipcam.axis.media.protocol.http.MjpegStream; -import org.sensorhub.api.data.DataEvent; -import org.sensorhub.api.sensor.SensorException; -import org.sensorhub.impl.sensor.AbstractSensorOutput; -import org.sensorhub.impl.sensor.videocam.VideoCamHelper; -import org.vast.data.DataBlockMixed; - - -/** - *- * Implementation of sensor interface for generic Axis Cameras using IP - * protocol. This particular class provides time-tagged video output from the video - * camera capabilities. - *
- * - * @author Mike Botts - * @since October 30, 2014 - */ -public class AxisVideoOutput extends AbstractSensorOutput < AxisCameraDriver > { - DataComponent videoDataStruct; - DataEncoding videoEncoding; - boolean reconnect; - boolean streaming; - - URL getImgSizeUrl; - - public AxisVideoOutput(AxisCameraDriver driver, String name) { - super(name, driver); - - try { - getImgSizeUrl = new URL(parentSensor.getHostUrl() + driver.VAPIX_QUERY_IMAGE_SIZE); - - } catch (MalformedURLException e) { - - e.printStackTrace(); - } - } - - - protected void init() throws SensorException { - try { - // get image size from camera HTTP interface - int[] imgSize = getImageSize(); - VideoCamHelper fac = new VideoCamHelper(); - - // build output structure - DataStream videoStream = fac.newVideoOutputMJPEG(getName(), imgSize[0], imgSize[1]); - videoDataStruct = videoStream.getElementType(); - videoEncoding = videoStream.getEncoding(); - } catch (Exception e) { - throw new SensorException("Error while initializing video output", e); - } - } - - - protected int[] getImageSize() throws IOException { - - BufferedReader reader = new BufferedReader(new InputStreamReader(getImgSizeUrl.openStream())); - - int imgSize[] = new int[2]; - String line; - while ((line = reader.readLine()) != null) { - // split line and parse each possible property - String[] tokens = line.split("="); - if (tokens[0].trim().equalsIgnoreCase("image width")) - imgSize[0] = Integer.parseInt(tokens[1].trim()); - else if (tokens[0].trim().equalsIgnoreCase("image height")) - imgSize[1] = Integer.parseInt(tokens[1].trim()); - } - - // index 0 is width, index 1 is height - return imgSize; - } - - - protected void start() { - try { - final URL videoUrl = new URL(parentSensor.getHostUrl().replace("axis-cgi", "mjpg/video.mjpg")); - - Thread t = new Thread(new Runnable() { - @Override - public void run() { - // send http query - try { - InputStream is = new BufferedInputStream(videoUrl.openStream()); - MjpegStream stream = new MjpegStream(is, null); - streaming = true; - - while (streaming) { - // extract next frame from MJPEG stream - Buffer buf = new Buffer(); - buf.setData(new byte[] {}); - stream.read(buf); - byte[] frameData = (byte[]) buf.getData(); - - // create new data block - DataBlock dataBlock; - if (latestRecord == null) - dataBlock = videoDataStruct.createDataBlock(); - else - dataBlock = latestRecord.renew(); - - //double timestamp = AXISJpegHeaderReader.getTimestamp(frameData) / 1000.; - double timestamp = System.currentTimeMillis() / 1000.; - dataBlock.setDoubleValue(0, timestamp); - //System.out.println(new DateTimeFormat().formatIso(timestamp, 0)); - - // uncompress to RGB bufferd image - /*InputStream imageStream = new ByteArrayInputStream(frameData); - ImageInputStream input = ImageIO.createImageInputStream(imageStream); - Iterator+ * Per-stream adapter that wires an {@link MpegTsProcessor} to a + * {@link VideoOutput} for a single Axis video URL (MJPEG over HTTP, + * or H.264 / H.265 over RTSP). + *
+ * + *+ * Each instance owns its own single-threaded decode executor and its own + * {@link MpegTsProcessor}. Multiple instances can coexist in the same + * Axis driver (e.g. one MJPEG + one H.264 output). + *
+ */ +class AxisVideoStream { + private static final Logger logger = LoggerFactory.getLogger(AxisVideoStream.class); + + private final AxisCameraDriver parent; + private final String outputName; + private final String sourceUrl; + private final String commandLineArgs; + + private MpegTsProcessor mpegTsProcessor; + private VideoOutput