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 readers = ImageIO.getImageReadersByMIMEType("image/jpeg"); - ImageReader reader = readers.next(); - reader.setInput(input); - //int width = reader.getWidth(0); - //int height = reader.getHeight(0); - - // The ImageTypeSpecifier object gives you access to more info such as - // bands, color model, etc. - //ImageTypeSpecifier imageType = reader.getRawImageType(0); - - BufferedImage rgbImage = reader.read(0); - byte[] byteData = ((DataBufferByte)rgbImage.getRaster().getDataBuffer()).getData(); - ((DataBlockMixed)dataBlock).getUnderlyingObject()[1].setUnderlyingObject(byteData);*/ - - // assign compressed data - ((DataBlockMixed) dataBlock).getUnderlyingObject()[1].setUnderlyingObject(frameData); - - latestRecord = dataBlock; - latestRecordTime = System.currentTimeMillis(); - eventHandler.publish(new DataEvent(latestRecordTime, AxisVideoOutput.this, latestRecord)); - } - - // wait 1s before trying to reconnect - Thread.sleep(1000); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - - t.start(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - @Override - public double getAverageSamplingPeriod() { - return 1 / 30.; - } - - - @Override - public DataComponent getRecordDescription() { - return videoDataStruct; - } - - - @Override - public DataEncoding getRecommendedEncoding() { - return videoEncoding; - } - - - public void stop() { - - } - -} \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoStream.java b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoStream.java new file mode 100644 index 000000000..20467ac4c --- /dev/null +++ b/sensors/video/sensorhub-driver-axis/src/main/java/org/sensorhub/impl/sensor/axis/AxisVideoStream.java @@ -0,0 +1,165 @@ +/***************************** 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) 2026 the Initial Developer. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.axis; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.ffmpeg.outputs.VideoOutput; +import org.sensorhub.mpegts.MpegTsProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + *

+ * 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 videoOutput; + private ExecutorService executor; + + /** + * @param parent Axis driver that owns this stream. + * @param outputName Name under which the resulting video output will be registered. + * @param sourceUrl FFmpeg-compatible source URL (http://..., rtsp://...). + * @param commandLineArgs Optional FFmpeg command-line-style options (e.g. "-timeout 3000000", + * or "-rtsp_transport tcp -stimeout 3000000"). May be {@code null}. + */ + AxisVideoStream(AxisCameraDriver parent, String outputName, String sourceUrl, String commandLineArgs) { + this.parent = parent; + this.outputName = outputName; + this.sourceUrl = sourceUrl; + this.commandLineArgs = commandLineArgs == null ? "" : commandLineArgs; + } + + /** + * Opens the remote stream, discovers its frame dimensions and codec, and + * creates + initializes the matching {@link VideoOutput}. The caller is + * responsible for calling {@code addOutput(getOutput(), false)} on the + * parent driver after this returns. + */ + void init() throws SensorException { + executor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "axis-ffmpeg-" + outputName); + t.setDaemon(true); + return t; + }); + + mpegTsProcessor = new MpegTsProcessor(sourceUrl, commandLineArgs); + // Live sources need extradata re-injection for late-join decoders + // (browser / CDS clients connecting after the first keyframe). + mpegTsProcessor.setInjectVideoExtradata(true); + + if (!mpegTsProcessor.openStream()) { + shutdownExecutorQuietly(); + throw new SensorException("Could not open FFmpeg stream: " + sourceUrl); + } + + if (!mpegTsProcessor.hasVideoStream()) { + closeStreamQuietly(); + shutdownExecutorQuietly(); + throw new SensorException("No video track found in stream: " + sourceUrl); + } + + int[] dims = mpegTsProcessor.getVideoStreamFrameDimensions(); + String codecName = mpegTsProcessor.getVideoCodecName(); + videoOutput = new VideoOutput<>(parent, dims, codecName, outputName, "Video", "Video stream via FFmpeg (" + codecName + ")"); + videoOutput.setExecutor(executor); + videoOutput.doInit(); + + mpegTsProcessor.setVideoDataBufferListener(videoOutput); + } + + /** + * The video output created by {@link #init()}. Returns {@code null} if + * {@code init()} was not called (or threw). + */ + VideoOutput getOutput() { + return videoOutput; + } + + /** + * Starts pulling frames from the remote stream into the video output. + * Safe to call only after {@link #init()} has succeeded. + */ + void start() throws SensorException { + if (mpegTsProcessor == null) + throw new SensorException("Stream has not been initialized"); + + try { + mpegTsProcessor.processStream(); + mpegTsProcessor.setReconnect(true); + } catch (IllegalStateException e) { + throw new SensorException("Failed to start FFmpeg stream: " + sourceUrl, e); + } + } + + /** + * Stops frame delivery and releases native resources. Safe to call multiple + * times and safe to call before {@link #init()}. + */ + void stop() { + if (mpegTsProcessor != null) { + try { + mpegTsProcessor.stopProcessingStream(); + mpegTsProcessor.join(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted while stopping FFmpeg stream {}", outputName, e); + } finally { + closeStreamQuietly(); + } + } + shutdownExecutorQuietly(); + } + + private void closeStreamQuietly() { + try { + if (mpegTsProcessor != null) + mpegTsProcessor.closeStream(); + } catch (Exception e) { + logger.warn("Error closing FFmpeg stream {}", outputName, e); + } finally { + mpegTsProcessor = null; + } + } + + private void shutdownExecutorQuietly() { + if (executor != null) { + executor.shutdownNow(); + executor = null; + } + } +} diff --git a/sensors/video/sensorhub-driver-axis/src/test/java/org/sensorhub/test/impl/sensor/axis/TestAxisCameraDriver.java b/sensors/video/sensorhub-driver-axis/src/test/java/org/sensorhub/test/impl/sensor/axis/TestAxisCameraDriver.java index fbba69839..b2c7e4a98 100644 --- a/sensors/video/sensorhub-driver-axis/src/test/java/org/sensorhub/test/impl/sensor/axis/TestAxisCameraDriver.java +++ b/sensors/video/sensorhub-driver-axis/src/test/java/org/sensorhub/test/impl/sensor/axis/TestAxisCameraDriver.java @@ -32,7 +32,7 @@ Developer are Copyright (C) 2014 the Initial Developer. All Rights Reserved. import org.sensorhub.impl.sensor.axis.AxisCameraConfig; import org.sensorhub.impl.sensor.axis.AxisCameraDriver; import org.sensorhub.impl.sensor.axis.AxisPtzOutput; -import org.sensorhub.impl.sensor.axis.AxisVideoOutput; +import org.sensorhub.impl.sensor.ffmpeg.outputs.VideoOutput; import org.sensorhub.test.sensor.videocam.VideoTestHelper; import org.vast.data.DataChoiceImpl; import org.vast.sensorML.SMLUtils; @@ -284,7 +284,7 @@ public void handleEvent(Event e) assertTrue(e instanceof DataEvent); DataEvent dataEvent = (DataEvent)e; - if (dataEvent.getSource().getClass().equals(AxisVideoOutput.class)) + if (dataEvent.getSource() instanceof VideoOutput) { videoTestHelper.renderFrameJPEG(dataEvent.getRecords()[0]); frameCount++;