diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDriver.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDriver.java deleted file mode 100644 index f28a7f409..000000000 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDriver.java +++ /dev/null @@ -1,504 +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. - - Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. - ******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.rtmp; - -import org.sensorhub.api.common.SensorHubException; -import org.sensorhub.api.module.ModuleEvent; -import org.sensorhub.api.sensor.SensorException; -import org.sensorhub.impl.sensor.AbstractSensorModule; -import org.sensorhub.impl.sensor.ffmpeg.outputs.AudioOutput; -import org.sensorhub.impl.sensor.ffmpeg.outputs.VideoOutput; -import org.sensorhub.impl.sensor.rtmp.config.RtmpConfig; -import org.sensorhub.mpegts.MpegTsProcessor; -import org.sensorhub.utils.Async; - -import java.security.SecureRandom; -import java.util.HexFormat; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; - -import static org.bytedeco.ffmpeg.global.avutil.av_log_set_callback; - -/** - * OpenSensorHub sensor module that listens for and processes RTMP streams. - *
- * The driver creates an FFmpeg-backed MPEG-TS processor configured as an RTMP - * listener. Once a publisher connects, the driver detects available audio and - * video streams, creates matching OpenSensorHub outputs, and forwards stream - * data through those outputs. - *
- *- * Only one RTMP driver instance may use a given port at a time. Port ownership - * is tracked through a shared {@link RtmpPortSingleton}. - *
- */ -public class RtmpDriver extends AbstractSensorModule- * If no unique identifier has been assigned, this method generates both the - * OpenSensorHub unique identifier and XML identifier from the configured - * serial number. It also releases any previously tracked port and rebuilds - * the RTMP listener URL from the current configuration. - *
- * - * @throws SensorHubException if initialization fails - */ - @Override - protected void doInit() throws SensorHubException { - super.doInit(); - - if (getUniqueIdentifier() == null) { - generateUniqueID("urn:osh:sensor:rtmp:", config.serialNumber); - generateXmlID("RTMP_", config.serialNumber); - } - - portSingleton.removeConnection(connectionPort); - - setConnectionUrl(); - - //createMpegTsProcessor(); - } - - private static String generateStreamKey() { - byte[] bytes = new byte[16]; - new SecureRandom().nextBytes(bytes); - return HexFormat.of().formatHex(bytes); - } - - /** - * Creates and stores the FFmpeg-backed MPEG-TS processor for the configured - * RTMP listener URL. - */ - private void createMpegTsProcessor() { - var mpegts = new MpegTsProcessor(connectionUrl, COMMAND_LINE_ARGS/* + " -rtmp_app /live -rtmp_playpath " + path*/); - mpegts.setInjectVideoExtradata(true); - mpegTsProcessor.set(mpegts); - } - - /** - * Builds the RTMP listener URL from the configured host and port. - * - * @throws SensorException if the connection configuration is invalid - */ - private void setConnectionUrl() throws SensorException { - - var connectionConfig = config.connectionConfig; - StringBuilder sb = new StringBuilder("rtmp://"); - - - /* - if (connectionConfig.host == HostType.OVERRIDE) { - if (connectionConfig.hostOverride == null || connectionConfig.hostOverride.isBlank()) { - throw new SensorException("Domain override is not set"); - } - sb.append(connectionConfig.hostOverride); - } else { - sb.append(connectionConfig.host.host); - } - - */ - - sb.append(connectionConfig.host.host); - - sb.append(":").append(connectionConfig.port); - - /* - if (connectionConfig.generateRandomStreamKey) { - connectionConfig.generateRandomStreamKey = false; - String streamKey = generateStreamKey(); - if (!connectionConfig.path.isBlank() && !connectionConfig.path.endsWith("/")) { - connectionConfig.path += "/"; - } - connectionConfig.path += streamKey; - } - - if (connectionConfig.path != null && !connectionConfig.path.isBlank()) { - if (!connectionConfig.path.startsWith("/")) { - connectionConfig.path = "/" + connectionConfig.path; - } - sb.append(connectionConfig.path); - } - - path = connectionConfig.path; - - */ - connectionUrl = sb.toString(); - connectionPort = connectionConfig.port; - } - - /** - * Starts the driver by reserving the configured RTMP port. - * - * @throws SensorHubException if the configured port is already in use by - * another RTMP driver module - */ - @Override - protected void doStart() throws SensorHubException { - String moduleUid; - if ((moduleUid = portSingleton.addConnection(connectionPort, this.getUniqueIdentifier())) != null) { - throw new SensorException("Port "+ connectionPort + " already in use by module: " + moduleUid); - } - } - - /** - * Performs post-start setup for RTMP listening and heartbeat monitoring. - *- * This method creates a fresh MPEG-TS processor, stops any previous executor - * services, reports the listening URL, starts the stream listener thread, and - * schedules periodic heartbeat checks. - *
- * - * @throws SensorHubException if executor shutdown is interrupted - */ - @Override - protected void afterStart() throws SensorHubException { - super.afterStart(); - //stopStream(); - hasConnected = false; - - synchronized (mpegTsProcessor) { - var mpegts = mpegTsProcessor.get(); - if (mpegts != null && mpegts.getState() != Thread.State.NEW) { - stopStream(); - } - createMpegTsProcessor(); - } - - try { - stopExecutors(); - } catch (InterruptedException e) { - throw new SensorHubException("Interrupted while stopping executors", e); - } - - reportStatus("Listening on: " + connectionUrl); - executorService = Executors.newSingleThreadExecutor(); - executorService.submit(this::startStream); - heartbeatExecutorService = Executors.newSingleThreadScheduledExecutor(); - heartbeatExecutorService.scheduleAtFixedRate(this::heartbeat, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, HEARTBEAT_TIME_UNIT); - heartbeatExecutorService.submit(this::heartbeat); - } - - /** - * Determines whether the module is currently stopping or stopped. - * - * @return {@code true} if the module is stopping or stopped; otherwise {@code false} - */ - private boolean isStopping () { - return getCurrentState() == ModuleEvent.ModuleState.STOPPING || getCurrentState() == ModuleEvent.ModuleState.STOPPED; - } - - /* - private boolean isMatchingPath(String url) { - boolean isMatching = url != null && url.trim().contains(path); - if (!isMatching) { - logger.warn("Received stream on: {} but expected: {}", url, config.connectionConfig.path); - } - return isMatching; - } - - */ - - /** - * Opens the RTMP listener, waits for an incoming stream, creates audio/video - * outputs for detected streams, and begins processing stream data. - *- * The method waits until the module reaches the {@code STARTED} state before - * adding outputs. If the stream cannot be opened, an error is reported and - * processing is not started. - *
- */ - private void startStream() { - // Need to wait for STARTED state so that outputs can be added - try { - Async.waitForCondition(() -> getCurrentState() == ModuleEvent.ModuleState.STARTED, MAX_STARTUP_WAIT_TIME_MS); - } catch (TimeoutException e) { - reportError("Failed to start stream; timed out waiting for startup", e); - return; - } - - boolean status; - - var mpegts = mpegTsProcessor.get(); - - if (mpegts == null) { - logger.error("Could not start; stream processor is null"); - return; - } - - /* - // Reject connections that don't match the configured path - // Thread will sit here until a matching connection is made - do { - if (Thread.currentThread().isInterrupted() || isStopping()) { - return; - } - mpegts.closeStream(); - status = mpegts.openStream(); - String path = mpegts.getPrivDataString("rtmp_app") + "/" + mpegts.getPrivDataString("rtmp_playpath"); - } while (!isMatchingPath(path)); - - */ - - mpegts.closeStream(); - status = mpegts.openStream(); - - if (isStopping()) { - return; - } - - if (!status) { - String error = "Failed to connect to " + connectionUrl; - reportError(error, new SensorException(error)); - return; - } - - synchronized (mpegTsProcessor) { - mpegts = mpegTsProcessor.get(); - - if (mpegts == null) { - reportError("Stream could not be opened", new SensorException("MpegTs processor is null")); - return; - } - - if (mpegts.isStreamOpened()) { - if (mpegts.hasVideoStream()) { - createVideoOutput(mpegts.getVideoStreamFrameDimensions(), mpegts.getVideoCodecName()); - mpegts.setVideoDataBufferListener(videoOutput.get()); - } - - if (mpegts.hasAudioStream()) { - createAudioOutput(mpegts.getAudioSampleRate(), mpegts.getAudioCodecName()); - mpegts.setAudioDataBufferListener(audioOutput.get()); - } - - } else { - reportError("Stream could not be opened", new SensorException("RTMP stream connected but not opened")); - return; - } - clearStatus(); - reportStatus("RTMP stream for " + connectionUrl + " opened."); - hasConnected = true; - isConnected = true; - mpegts.processStream(); - /* - executorService.submit(() -> { - MpegTsProcessor processor; - while ((processor = mpegTsProcessor.get()) != null) { - - while (processor.isStreamOpened()) { - processor.processP(); - } - if (!Thread.currentThread().isInterrupted()) { - reportStatus("RTMP stream " + connectionUrl + " lost connection. Reconnecting..."); - processor.openStream(); - } else { - return; - } - } - reportStatus("RTMP stream closed."); - - }); - - */ - //mpegts.processStream(); - } - } - - /** - * Checks the active stream and attempts to reconnect if a previously - * connected stream has been lost. - */ - private void heartbeat() { - var mpegts = mpegTsProcessor.get(); - if (mpegts == null || !hasConnected) { return; } - - if (!mpegts.isStreamOpened()) { - reportStatus("RTMP stream " + connectionUrl + " lost connection. Reconnecting..."); - isConnected = false; - createMpegTsProcessor(); - startStream(); - } - } - - /** - * Creates and registers the video output for the detected RTMP video stream. - * - * @param videoDims video frame dimensions, usually width and height - * @param codecName name of the detected video codec - */ - protected void createVideoOutput(int[] videoDims, String codecName) { - synchronized (videoOutput) { - var videoOut = new VideoOutput<>(this, videoDims, codecName); - videoOutput.set(videoOut); - - if (videoExecutorService != null) { - videoExecutorService.shutdown(); - } - - videoExecutorService = Executors.newSingleThreadExecutor(); - videoOut.setExecutor(videoExecutorService); - videoOut.doInit(); - addOutput(videoOut, false); - } - } - - /** - * Creates and registers the audio output for the detected RTMP audio stream. - * - * @param sampleRate detected audio sample rate in hertz - * @param codecName name of the detected audio codec - */ - protected void createAudioOutput(int sampleRate, String codecName) { - synchronized (audioOutput) { - var audioOut = new AudioOutput<>(this, sampleRate, codecName); - audioOutput.set(audioOut); - - if (audioExecutorService != null) { - audioExecutorService.shutdown(); - } - audioExecutorService = Executors.newSingleThreadExecutor(); - audioOut.setExecutor(audioExecutorService); - audioOut.doInit(); - addOutput(audioOut, false); - } - } - - /** - * Stops the driver and releases all RTMP stream resources. - * - * @throws SensorHubException if shutdown fails - */ - @Override - protected void doStop() throws SensorHubException { - super.doStop(); - shutdown(); - } - - /** - * Releases the reserved port, stops the stream, and terminates executor services. - * - * @throws SensorHubException if executor shutdown is interrupted - */ - private void shutdown() throws SensorHubException { - portSingleton.removeConnection(config.connectionConfig.port); - stopStream(); - try { - stopExecutors(); - } catch (InterruptedException e) { - throw new SensorHubException("Interrupted while stopping executors", e); - } - - } - - /** - * Stops stream processing, waits for the MPEG-TS processor thread to finish, - * closes the stream, and clears the processor reference. - */ - private void stopStream() { - isConnected = false; - synchronized (mpegTsProcessor) { - var mpegts = mpegTsProcessor.get(); - if (mpegts != null) { - mpegts.stopProcessingStream(); - if (mpegts.isAlive()) { - try { - logger.info("Waiting for stream to stop."); - mpegts.join(); - } catch (InterruptedException e) { - logger.error("Interrupted while waiting for stream to stop.", e); - } - } - mpegts.closeStream(); - } - mpegTsProcessor.set(null); - } - } - - /** - * Stops all executor services used by the driver and waits for termination. - * - * @throws InterruptedException if interrupted while waiting for executor termination - */ - private void stopExecutors() throws InterruptedException { - if (executorService != null) { - executorService.shutdownNow(); - executorService.awaitTermination(EXECUTOR_JOIN_TIMEOUT, EXECUTOR_JOIN_TIME_UNIT); - } - if (videoExecutorService != null) { - videoExecutorService.shutdownNow(); - videoExecutorService.awaitTermination(EXECUTOR_JOIN_TIMEOUT, EXECUTOR_JOIN_TIME_UNIT); - } - if (audioExecutorService != null) { - audioExecutorService.shutdownNow(); - audioExecutorService.awaitTermination(EXECUTOR_JOIN_TIMEOUT, EXECUTOR_JOIN_TIME_UNIT); - } - if (heartbeatExecutorService != null) { - heartbeatExecutorService.shutdownNow(); - heartbeatExecutorService.awaitTermination(EXECUTOR_JOIN_TIMEOUT, EXECUTOR_JOIN_TIME_UNIT); - } - } - - /** - * Cleans up module resources before disposal. - * - * @throws SensorHubException if cleanup or shutdown fails - */ - @Override - public void cleanup() throws SensorHubException { - super.cleanup(); - shutdown(); - } - - /** - * Indicates whether the driver is currently started and has an open RTMP stream. - * - * @return {@code true} if the module is started and the RTMP stream is open; - * otherwise {@code false} - */ - @Override - public boolean isConnected() { - return isConnected; - } -} \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpPortSingleton.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpPortSingleton.java deleted file mode 100644 index 1f1bc4098..000000000 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpPortSingleton.java +++ /dev/null @@ -1,65 +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. - - Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. - ******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.rtmp; - -import java.util.HashMap; -import java.util.Map; - -/** - * Tracks RTMP listener ports currently reserved by RTMP driver modules. - *- * The singleton prevents multiple RTMP driver instances from attempting to listen - * on the same port at the same time. All public methods are synchronized to - * provide simple thread-safe access to the port reservation map. - *
- */ -public final class RtmpPortSingleton { - private static final RtmpPortSingleton instance = new RtmpPortSingleton(); - - private final Map- * If the port is not already reserved, this method records the module unique - * identifier and returns {@code null}. If the port is already reserved, this - * method returns the unique identifier of the module that currently owns it. - *
- * - * @param url RTMP listener port to reserve - * @param moduleUid unique identifier of the module requesting the port - * @return {@code null} if the reservation succeeded; otherwise the unique - * identifier of the module currently using the port - */ - public synchronized String addConnection(int url, String moduleUid) { - if (urls.containsKey(url)) { - return urls.get(url); - } else { - urls.put(url, moduleUid); - return null; - } - } - - /** - * Releases a previously reserved RTMP listener port. - * - * @param url RTMP listener port to release - */ - public synchronized void removeConnection(int url) { - urls.remove(url); - } -} diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/ConnectionConfig.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/ConnectionConfig.java deleted file mode 100644 index 5f8900d5f..000000000 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/ConnectionConfig.java +++ /dev/null @@ -1,43 +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. - - Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. - ******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.rtmp.config; - -import org.sensorhub.api.config.DisplayInfo; - -public class ConnectionConfig { - - /* - @DisplayInfo.Required - @DisplayInfo(label = "Generate Random Stream Key", desc = "Enable to generate and append a random hex string to the path. " + - "Recommended for security. Only enable on first init, otherwise path will include multiple keys. ") - public boolean generateRandomStreamKey = true; - - */ - - @DisplayInfo.Required - @DisplayInfo(label = "Host", desc = "Domain listening for an RTMP connection request. Unspecified should work " + - "for most cases.") - public HostType host = HostType.UNSPECIFIED; - - @DisplayInfo.Required - @DisplayInfo(label = "Port", desc = "Port listening for an RTMP connection request.") - @DisplayInfo.ValueRange(min = 1, max = 65535) - public int port = 1935; - - /* - @DisplayInfo(label = "Path", desc = "(Optional) Path to listen for an RTMP connection request. I.e. everything in the URL " + - "after the port.") - public String path = ""; - - */ -} \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/HostType.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/HostType.java deleted file mode 100644 index cd72f7f5b..000000000 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/HostType.java +++ /dev/null @@ -1,26 +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. - - Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. - ******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.impl.sensor.rtmp.config; - -public enum HostType { - UNSPECIFIED("0.0.0.0"), - LOCALHOST("localhost"), - DOCKER_INTERNAL("host.docker.internal")/*, - OVERRIDE("")*/; - - public final String host; - - HostType(String host) { - this.host = host; - } -} diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/Activator.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/Activator.java similarity index 95% rename from sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/Activator.java rename to sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/Activator.java index 8f3900c58..935857b5f 100644 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/Activator.java +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/Activator.java @@ -10,7 +10,7 @@ Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. ******************************* END LICENSE BLOCK ***************************/ -package org.sensorhub.impl.sensor.rtmp; +package org.sensorhub.impl.sensor.rtmpcam; import org.osgi.framework.BundleActivator; import org.sensorhub.utils.OshBundleActivator; diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDescriptor.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDescriptor.java similarity index 91% rename from sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDescriptor.java rename to sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDescriptor.java index 85396aed1..3f3af6b82 100644 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/RtmpDescriptor.java +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDescriptor.java @@ -10,13 +10,13 @@ Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. ******************************* END LICENSE BLOCK ***************************/ -package org.sensorhub.impl.sensor.rtmp; +package org.sensorhub.impl.sensor.rtmpcam; import org.sensorhub.api.module.IModule; import org.sensorhub.api.module.IModuleProvider; import org.sensorhub.api.module.ModuleConfig; import org.sensorhub.impl.module.JarModuleProvider; -import org.sensorhub.impl.sensor.rtmp.config.RtmpConfig; +import org.sensorhub.impl.sensor.rtmpcam.config.RtmpConfig; public class RtmpDescriptor extends JarModuleProvider implements IModuleProvider diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDriver.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDriver.java new file mode 100644 index 000000000..c0539a4ec --- /dev/null +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/RtmpDriver.java @@ -0,0 +1,199 @@ +/***************************** 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. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.rtmpcam; + +import org.sensorhub.api.common.SensorHubException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.ffmpeg.outputs.AudioOutput; +import org.sensorhub.impl.sensor.ffmpeg.outputs.VideoOutput; +import org.sensorhub.impl.sensor.rtmpcam.config.ConnectionConfig; +import org.sensorhub.impl.sensor.rtmpcam.config.RtmpConfig; +import org.sensorhub.impl.sensor.rtmpcam.connection.RtmpListener; +import org.sensorhub.impl.sensor.rtmpcam.connection.RtmpListenerManager; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpConnectEvent; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpDisconnectEvent; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpReconnectEvent; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpStreamEvent; +import org.sensorhub.mpegts.MpegTsProcessor; + +import java.security.SecureRandom; +import java.util.HexFormat; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import static org.bytedeco.ffmpeg.global.avutil.av_log_set_callback; + + +/** + * RtmpDriver is a class that provides the implementation for managing an RTMP stream as part of + * a sensor module. This class initiates, monitors, and handles connections to RTMP streams. It also + * handles video and audio stream processing based on the RTMP protocol. + */ +public class RtmpDriver extends AbstractSensorModule+ * If no unique identifier has been assigned, this method generates both the + * OpenSensorHub unique identifier and XML identifier from the configured + * serial number. It also releases any previously tracked port and rebuilds + * the RTMP listener URL from the current configuration. + *
+ * + * @throws SensorHubException if initialization fails + */ + @Override + protected void doInit() throws SensorHubException { + super.doInit(); + + if (getUniqueIdentifier() == null) { + generateUniqueID("urn:osh:sensor:rtmp:", config.serialNumber); + generateXmlID("RTMP_", config.serialNumber); + } + if (config.connectionConfig.generateRandomKey) { + config.connectionConfig.streamKey = generateStreamKey(); + config.connectionConfig.generateRandomKey = false; + } + + rtmpListenerManager.removeListener(this); + } + + @Override + protected void doStart() throws SensorHubException { + super.doStart(); + reportStatus("RTMP: Listening for connection"); + rtmpListenerManager.addListener(this); + doStreamProcessing = true; + } + + private static String generateStreamKey() { + byte[] bytes = new byte[16]; + new SecureRandom().nextBytes(bytes); + return HexFormat.of().formatHex(bytes); + } + + public boolean doStreamProcessing() { + return doStreamProcessing; + } + + + /** + * Stops the driver and releases all RTMP stream resources. + * + * @throws SensorHubException if shutdown fails + */ + @Override + protected void doStop() throws SensorHubException { + super.doStop(); + doStreamProcessing = false; + rtmpListenerManager.removeListener(this); + } + + /** + * Indicates whether the driver is currently started and has an open RTMP stream. + * + * @return {@code true} if the module is started and the RTMP stream is open; + * otherwise {@code false} + */ + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public ConnectionConfig config() { + return config.connectionConfig; + } + + @Override + public void onConnected(RtmpConnectEvent event) { + reportStatus("Connected to: " + connectionUrl); + } + + /** + * Handles the event triggered when an RTMP stream is connected. + * This method initializes both video and audio output streams based on the given stream information + * contained in the event payload. If the stream payload is null, a warning will be logged, and no further + * action will be taken. + * + * @param event the event containing the payload with stream information, including video and audio codec + * details and video dimensions or audio sample rate information required to initialize the outputs. + */ + @Override + public void onStreamConnected(RtmpStreamEvent event) { + var streamInfo = event.getPayload(); + + if (streamInfo == null) { + logger.warn("StreamInfo is null"); + return; + } + if (streamInfo.videoCodec() != null) { + videoOutput.set(new VideoOutput<>(this, streamInfo.videoDimensions(), streamInfo.videoCodec())); + videoExecutorService = Executors.newSingleThreadExecutor(); + videoOutput.get().setExecutor(videoExecutorService); + videoOutput.get().doInit(); + addOutput(videoOutput.get(), false); + + } + if (streamInfo.audioCodec() != null) { + audioOutput.set(new AudioOutput<>(this, streamInfo.audioSampleRate(), streamInfo.audioCodec())); + audioExecutorService = Executors.newSingleThreadExecutor(); + audioOutput.get().setExecutor(audioExecutorService); + audioOutput.get().doInit(); + addOutput(audioOutput.get(), false); + } + reportStatus("RTMP: Connected"); + } + + @Override + public void onDisconnected(RtmpDisconnectEvent event) { + removeAllOutputs(); + reportStatus("RTMP: Disconnected"); + } + + @Override + public void onReconnected(RtmpReconnectEvent event) { + reportStatus("RTMP: Connected"); + } + + @Override + public VideoOutput> getVideoOutput() { + return this.videoOutput.get(); + } + + @Override + public AudioOutput> getAudioOutput() { + return this.audioOutput.get(); + } +} \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/ConnectionConfig.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/ConnectionConfig.java new file mode 100644 index 000000000..fa7fe3671 --- /dev/null +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/ConnectionConfig.java @@ -0,0 +1,70 @@ +/***************************** 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. + + Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.rtmpcam.config; + +import org.sensorhub.api.config.DisplayInfo; +import org.sensorhub.impl.sensor.rtmpcam.connection.RtmpConnectionContext; + +public class ConnectionConfig { + + /* + @DisplayInfo.Required + @DisplayInfo(label = "Generate Random Stream Key", desc = "Enable to generate and append a random hex string to the path. " + + "Recommended for security. Only enable on first init, otherwise path will include multiple keys. ") + public boolean generateRandomStreamKey = true; + + */ + + @DisplayInfo(label = "Username") + public String username = ""; + + @DisplayInfo(label = "Password") + public String password = ""; + + @DisplayInfo.Required + @DisplayInfo(label = "Port", desc = "Port listening for an RTMP connection request.") + @DisplayInfo.ValueRange(min = 1, max = 65535) + public int port = 1935; + + @DisplayInfo(label = "Path") + public String path = ""; + + @DisplayInfo(label = "Stream Key", desc = "(Optional) Stream key to use for the RTMP connection.") + public String streamKey = ""; + + @DisplayInfo(label = "Generate Random Stream Key", desc = "Overwrite the stream key field with a random string of characters.") + public boolean generateRandomKey = false; + + /** Key for this config's exact fields — used as the map key on registration. */ + public String compositeKey() { + return compositeKey(username, password, port, path, streamKey); + } + + /** + * Static form used by the router to generate wildcard candidate keys + * (null fields become empty strings, producing a distinct key per specificity level). + * + * Format: "username:password:port:path:streamKey" + * Example: "alice:secret:1935:live:cam1" + * Catch-all: "::1935::" + */ + public static String compositeKey(String username, String password, int port, String path, String streamKey) { + return (username != null ? username : "") + ":" + + (password != null ? password : "") + ":" + + port + ":" + + (path != null ? path : "") + ":" + + (streamKey != null ? streamKey : ""); + } + + public static String compositeKey(RtmpConnectionContext ctx) { return compositeKey(ctx.username(), ctx.password(), ctx.port(), ctx.path(), ctx.streamKey()); } +} \ No newline at end of file diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/RtmpConfig.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/RtmpConfig.java similarity index 97% rename from sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/RtmpConfig.java rename to sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/RtmpConfig.java index d466a4ba0..43ae02853 100644 --- a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmp/config/RtmpConfig.java +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/config/RtmpConfig.java @@ -10,7 +10,7 @@ Copyright (C) 2026 GeoRobotix Innovative Research, Inc. All Rights Reserved. ******************************* END LICENSE BLOCK ***************************/ -package org.sensorhub.impl.sensor.rtmp.config; +package org.sensorhub.impl.sensor.rtmpcam.config; import org.sensorhub.api.config.DisplayInfo; import org.sensorhub.api.sensor.PositionConfig; diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionContext.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionContext.java new file mode 100644 index 000000000..eb38056fd --- /dev/null +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionContext.java @@ -0,0 +1,9 @@ +package org.sensorhub.impl.sensor.rtmpcam.connection; + +public record RtmpConnectionContext( + int port, + String username, + String password, + String path, + String streamKey +) {} diff --git a/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionHandler.java b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionHandler.java new file mode 100644 index 000000000..00d9c6046 --- /dev/null +++ b/sensors/video/sensorhub-driver-rtmp/src/main/java/org/sensorhub/impl/sensor/rtmpcam/connection/RtmpConnectionHandler.java @@ -0,0 +1,268 @@ +package org.sensorhub.impl.sensor.rtmpcam.connection; + +import org.bytedeco.ffmpeg.avcodec.AVCodec; +import org.bytedeco.ffmpeg.avcodec.AVPacket; +import org.bytedeco.ffmpeg.avformat.*; +import org.bytedeco.ffmpeg.avutil.AVDictionary; +import org.bytedeco.ffmpeg.avutil.AVRational; +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.Pointer; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpConnectEvent; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpDisconnectEvent; +import org.sensorhub.impl.sensor.rtmpcam.event.RtmpStreamEvent; +import org.sensorhub.impl.sensor.rtmpcam.stream.StreamInfo; +import org.sensorhub.mpegts.DataBufferListener; +import org.sensorhub.mpegts.StreamContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.*; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.bytedeco.ffmpeg.global.avcodec.*; +import static org.bytedeco.ffmpeg.global.avformat.*; +import static org.bytedeco.ffmpeg.global.avutil.*; + +/** + * Handles one RTMP connection: + * 1. Handshake + * 2. AMF0 negotiation → {@link RtmpConnectionContext} + * 3. Route to a matching {@link RtmpListener} + * 4. RTMP chunks → FLV pipe → FFmpeg custom AVIO + * 5. Deliver encoded packets + */ +class RtmpConnectionHandler { + + private static final int AVIO_BUF = 64 * 1024; + private static final Logger logger = LoggerFactory.getLogger(RtmpConnectionHandler.class); + + private final Socket socket; + private final int port; + private final RtmpListenerManager manager; + + private final StreamContext videoStreamContext = new StreamContext(); + private final StreamContext audioStreamContext = new StreamContext(); + private final StreamContext dataStreamContext = new StreamContext(); + + private final Map
+ * When the difference between the total bytes received and the bytes
+ * already acknowledged reaches or exceeds the window acknowledgment size,
+ * this method sends an acknowledgment for the total number of bytes received
+ * so far.
+ */
+ private void maybeAck() throws IOException {
+ if (windowAckSize > 0 && (bytesReceived - bytesAcked) >= windowAckSize) {
+ bytesAcked = bytesReceived;
+ sendAck(bytesReceived);
+ }
+ }
+
+ /**
+ * Reads and decodes an AMF0 (Action Message Format 0) value from the provided input stream.
+ * The AMF0 format is a binary serialization format used in Adobe Flash and other related technologies.
+ * The method interprets a type byte to identify the kind of data being read and processes it accordingly.
+ *
+ * @param src the input stream from which the AMF0 value is read.
+ * It must be properly positioned to read the type and value bytes.
+ * @return the deserialized object corresponding to the AMF0 type.
+ * May return {@code null} for certain AMF0 types such as null, undefined, object end, or date (skipped).
+ * @throws IOException if an I/O error occurs while reading from the input stream or if an unknown type is encountered.
+ */
+ private Object readAmf0Value(DataInputStream src) throws IOException {
+ int type = src.readUnsignedByte();
+ return switch (type) {
+ case 0 -> src.readDouble();
+ case 1 -> src.readUnsignedByte() != 0;
+ case 2 -> readAmf0Utf8(src);
+ case 3 -> readAmf0Object(src);
+ case 5, 6 -> null; // Null, Undefined
+ case 8 -> readAmf0EcmaArray(src);
+ case 9 -> null; // ObjectEnd (context-terminator)
+ case 10 -> readAmf0StrictArray(src);
+ case 11 -> { src.skipBytes(10); yield null; } // Date — skip
+ case 12 -> readAmf0LongString(src);
+ default -> throw new IOException("Unknown AMF0 type: 0x" + Integer.toHexString(type));
+ };
+ }
+
+ private String readAmf0Utf8(DataInputStream src) throws IOException {
+ byte[] b = new byte[src.readUnsignedShort()];
+ src.readFully(b);
+ return new String(b, StandardCharsets.UTF_8);
+ }
+
+ private String readAmf0LongString(DataInputStream src) throws IOException {
+ byte[] b = new byte[src.readInt()];
+ src.readFully(b);
+ return new String(b, StandardCharsets.UTF_8);
+ }
+
+ private Map