diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..716dd3a4 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,38 @@ +--- +Checks: > + clang-analyzer-*, + -clang-analyzer-optin.cplusplus.VirtualCall, + clang-diagnostic-*, + google-*, + misc-*, + -misc-non-private-member-variables-in-classes, + readability-*, + -readability-identifier-length, + -readability-magic-numbers, + +CheckOptions: + misc-include-cleaner.IgnoreHeaders: bits/chrono.h + readability-function-cognitive-complexity.IgnoreMacros: true + readability-identifier-naming.ClassCase: CamelCase + readability-identifier-naming.ClassMemberCase: lower_case + readability-identifier-naming.ConstexprVariableCase: CamelCase + readability-identifier-naming.EnumCase: CamelCase + readability-identifier-naming.EnumConstantCase: CamelCase + readability-identifier-naming.FunctionCase: lower_case + readability-identifier-naming.GlobalConstantCase: CamelCase + readability-identifier-naming.StaticConstantCase: lower_case + readability-identifier-naming.StaticVariableCase: lower_case + readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + readability-identifier-naming.MacroDefinitionIgnoredRegexp: '^[A-Z]+(_[A-Z]+)*_$' + readability-identifier-naming.PrivateMemberCase: lower_case + readability-identifier-naming.ProtectedMemberCase: lower_case + readability-identifier-naming.PublicMemberCase: lower_case + readability-identifier-naming.PrivateMemberSuffix: _ + readability-identifier-naming.ProtectedMemberSuffix: _ + readability-identifier-naming.PublicMemberSuffix: '' + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.ParameterCase: lower_case + readability-identifier-naming.TypeAliasCase: CamelCase + readability-identifier-naming.TypedefCase: CamelCase + readability-identifier-naming.VariableCase: lower_case + readability-identifier-naming.IgnoreMainLikeFunctions: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..67d02af3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + workflow_dispatch: + push: + branches-ignore: + - ros1* + pull_request: + branches-ignore: + - ros1* + +jobs: + industrial_ci: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ROS_DISTRO: [humble, jazzy, kilted, rolling] + ROS_REPO: [testing, main] + env: + ROS_DISTRO: ${{ matrix.ROS_DISTRO }} + ROS_REPO: ${{ matrix.ROS_REPO }} + CMAKE_ARGS: ${{ matrix.ROS_DISTRO == 'rolling' && '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DENABLE_CLANG_TIDY=ON' || '' }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Source tests + uses: "ros-industrial/industrial_ci@master" diff --git a/AUTHORS.md b/AUTHORS.md index cf18e302..ff244bb5 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,9 +1,8 @@ -Original Authors ----------------- +# Original Authors - * Mitchell Wills (mwills@wpi.edu) + * Mitchell Wills -Contributors ------------- +# Contributors - * [Russell Toris](http://users.wpi.edu/~rctoris/) (rctoris@wpi.edu) + * Russell Toris + * Błażej Sowa diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 785d3a9d..d37fd346 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,59 @@ Changelog for package web_video_server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +3.0.0 (2026-01-30) +------------------ +* refactor: Add clang-tidy checks (#195) +* feat: Use pluginlib to load streamer plugins at runtime, general refactor (#192) +* Contributors: Błażej Sowa + +2.1.1 (2025-09-02) +------------------ +* Fix build with the current FFmpeg avformat (#190) +* Update package.xml to include necessary lib boost (#186) +* Add Kilted workflow (#183) +* Fix -Wmaybe-uninitialized warning (#185) +* Contributors: Błażej Sowa, Fabian Freihube, Janosch Machowinski, Alexis Tsogias, Julian Francis + +2.1.0 (2025-05-21) +------------------ +* Use target_link_libraries instead of ament_target_dependencies (#182) +* Fix compile warnings (#176) +* Use chrono steady clock for frame timing (#173) +* Add pkg-config dependency (#175) +* Separate web_video_server into a component and an executable (#168) +* Contributors: Błażej Sowa, Fabian Freihube, Joe Dinius, Ph.D., Lars Lorentz Ludvigsen, Mat198 + +2.0.1 (2024-10-26) +------------------ +* Add ros_environment to test dependencies (#166) +* Contributors: Błażej Sowa + +2.0.0 (2024-10-11) +------------------ +* Replace boost with std (#164) +* Add ament_cpplint test, resolve TODOs (#162) +* Add license headers to all C++ source files, update copyrights (#161) +* Add support for alpha pngs by adding per stream type decode functions (backport #106) (#163) +* Add link to /stream in stream list (backport #118) (#160) +* Add support for jpg compression format (backport #142) (#159) +* Reformat the code with uncrustify (#158) +* Use hpp extension for headers (#157) +* Fix request logging, remove global parameters (#156) +* Replace nh with node (#155) +* Fix declaring and retrieving node parameters (#154) +* Fix usage of deprecated libavcodec functions (#150) +* Use cv_bridge hpp headers when available (#149) +* Use target_link_libraries instead of ament_target_dependencies where applicable +* Don't install headers +* Add CI workflow and ament_lint tests (#148) +* Update package maintainer +* allow topic searches to continue past invalid multi-type topics. (#146) +* Add QoS profile query parameters (#133) +* Fix build for ROS2 Humble (#129) +* Fix build for ROS2 Foxy (#111) +* Contributors: Błażej Sowa, Domenic Rodriguez, Robert Brothers, Sebastian Castro, Tina Tian, TobinHall, Matthew Bries + 1.0.0 (2019-09-20) ------------------ * Port to ROS 2 diff --git a/CMakeLists.txt b/CMakeLists.txt index ce84c17b..6e31586b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,23 @@ cmake_minimum_required(VERSION 3.5) project(web_video_server) +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + find_package(ament_cmake_ros REQUIRED) find_package(async_web_server_cpp REQUIRED) find_package(cv_bridge REQUIRED) find_package(image_transport REQUIRED) +find_package(pluginlib REQUIRED) find_package(rclcpp REQUIRED) +find_package(rclcpp_components REQUIRED) +find_package(rmw REQUIRED) find_package(sensor_msgs REQUIRED) find_package(OpenCV REQUIRED) -find_package(Boost REQUIRED COMPONENTS thread) +find_package(Boost REQUIRED COMPONENTS system) find_package(PkgConfig REQUIRED) pkg_check_modules(avcodec libavcodec REQUIRED) @@ -30,35 +37,67 @@ endif() ## Build ## ########### -## Specify additional locations of header files -include_directories(include - ${Boost_INCLUDE_DIRS} - ${avcodec_INCLUDE_DIRS} - ${avformat_INCLUDE_DIRS} - ${avutil_INCLUDE_DIRS} - ${swscale_INCLUDE_DIRS} -) +if(${cv_bridge_VERSION} VERSION_LESS "3.3.0") + add_compile_definitions(CV_BRIDGE_USES_OLD_HEADERS) +endif() -## Declare a cpp executable -add_executable(${PROJECT_NAME} +## Declare a cpp library +add_library(${PROJECT_NAME} SHARED src/web_video_server.cpp - src/image_streamer.cpp - src/libav_streamer.cpp - src/vp8_streamer.cpp - src/h264_streamer.cpp - src/vp9_streamer.cpp src/multipart_stream.cpp - src/ros_compressed_streamer.cpp - src/jpeg_streamers.cpp - src/png_streamers.cpp + src/streamer.cpp + src/utils.cpp ) -ament_target_dependencies(${PROJECT_NAME} - async_web_server_cpp cv_bridge image_transport rclcpp sensor_msgs) +target_include_directories(${PROJECT_NAME} + PUBLIC + "$" + "$" +) ## Specify libraries to link a library or executable target against target_link_libraries(${PROJECT_NAME} - ${Boost_LIBRARIES} + PUBLIC + async_web_server_cpp::async_web_server_cpp + pluginlib::pluginlib + rclcpp::rclcpp + rmw::rmw + Boost::boost + PRIVATE + rclcpp_components::component +) + +add_library(${PROJECT_NAME}_streamers SHARED + src/streamers/image_transport_streamer.cpp + src/streamers/libav_streamer.cpp + src/streamers/h264_streamer.cpp + src/streamers/jpeg_streamers.cpp + src/streamers/png_streamers.cpp + src/streamers/ros_compressed_streamer.cpp + src/streamers/vp8_streamer.cpp + src/streamers/vp9_streamer.cpp +) + +target_include_directories(${PROJECT_NAME}_streamers + PUBLIC + "$" + "$" + ${avcodec_INCLUDE_DIRS} + ${avformat_INCLUDE_DIRS} + ${avutil_INCLUDE_DIRS} + ${swscale_INCLUDE_DIRS} +) + +target_link_libraries(${PROJECT_NAME}_streamers + ${PROJECT_NAME} + async_web_server_cpp::async_web_server_cpp + cv_bridge::cv_bridge + image_transport::image_transport + pluginlib::pluginlib + rclcpp::rclcpp + ${sensor_msgs_TARGETS} + Boost::boost + Boost::system ${OpenCV_LIBS} ${avcodec_LIBRARIES} ${avformat_LIBRARIES} @@ -66,18 +105,80 @@ target_link_libraries(${PROJECT_NAME} ${swscale_LIBRARIES} ) +## Declare a cpp executable +add_executable(${PROJECT_NAME}_node + src/web_video_server_node.cpp +) + +target_link_libraries(${PROJECT_NAME}_node + ${PROJECT_NAME} +) + +rclcpp_components_register_nodes(${PROJECT_NAME} "web_video_server::WebVideoServer") + +pluginlib_export_plugin_description_file(web_video_server plugins.xml) + ############# ## Install ## ############# -## Mark executables and/or libraries for installation -install(TARGETS ${PROJECT_NAME} - DESTINATION lib/${PROJECT_NAME} +install( + DIRECTORY include/ + DESTINATION include/${PROJECT_NAME} +) + +install( + TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_streamers + EXPORT export_${PROJECT_NAME} + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin +) +ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) +ament_export_dependencies( + async_web_server_cpp + cv_bridge + image_transport + pluginlib + rclcpp + rmw + sensor_msgs ) -install(DIRECTORY include/${PROJECT_NAME}/ - DESTINATION include/${PROJECT_NAME} - FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" +set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME ${PROJECT_NAME}) +install( + TARGETS ${PROJECT_NAME}_node + DESTINATION lib/${PROJECT_NAME} ) -ament_package() +########### +## Tests ## +########### + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + + # Skip ament_copyright check for humble + if($ENV{ROS_DISTRO} STREQUAL "humble") + list(APPEND AMENT_LINT_AUTO_EXCLUDE + ament_cmake_copyright + ) + endif() + + option(ENABLE_CLANG_TIDY "Enable ament_clang_tidy test" OFF) + if(ENABLE_CLANG_TIDY) + include(ProcessorCount) + processorcount(N) + set(ament_cmake_clang_tidy_JOBS ${N}) + else() + list(APPEND AMENT_LINT_AUTO_EXCLUDE + ament_cmake_clang_tidy + ) + endif() + + ament_lint_auto_find_test_dependencies() +endif() + +ament_package( + CONFIG_EXTRAS "config-extras.cmake" +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5687d9bb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +Any contribution that you make to this repository will +be under the 3-Clause BSD License, as dictated by that +[license](https://opensource.org/licenses/BSD-3-Clause). \ No newline at end of file diff --git a/LICENSE b/LICENSE index 872a63f6..4a3a9e6d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,32 +1,29 @@ -Software License Agreement (BSD License) - Copyright (c) 2014, Worcester Polytechnic Institute +Copyright (c) 2024, The Robot Web Tools Contributors All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Worcester Polytechnic Institute - nor the names of its contributors may be used to - endorse or promote products derived from this software without - specific prior written permission. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index cbe4568e..98802041 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,217 @@ -web_video_server [![Build Status](https://api.travis-ci.org/RobotWebTools/web_video_server.png)](https://travis-ci.org/RobotWebTools/web_video_server) -================ +# web_video_server - HTTP Streaming of ROS Topics in Multiple Formats -#### HTTP Streaming of ROS Image Topics in Multiple Formats -This node combines the capabilities of [ros_web_video](https://github.com/RobotWebTools/ros_web_video) and [mjpeg_server](https://github.com/RobotWebTools/mjpeg_server) into a single node. +This node provides HTTP streaming of ROS topics in various formats, making it easy to view robot camera feeds and other topics in a web browser without requiring special plugins or extensions. -For full documentation, see [the ROS wiki](http://ros.org/wiki/web_video_server). +## Features -[Doxygen](http://docs.ros.org/indigo/api/web_video_server/html/) files can be found on the ROS wiki. +- Stream ROS image topics over HTTP in multiple formats: + - MJPEG (Motion JPEG) + - VP8 (WebM) + - VP9 (WebM) + - H264 (MP4) + - PNG streams + - ROS compressed image streams +- Query snapshots of image topics in multiple formats: + - JPEG + - PNG + - ROS compressed image +- Plugin-based architecture for easy addition of new streaming formats +- Adjustable quality, size, and other streaming parameters +- Web interface to browse available image topics +- Support for different QoS profiles in ROS 2 -This project is released as part of the [Robot Web Tools](http://robotwebtools.org/) effort. +## Installation + +### Dependencies + +- ROS (Noetic) or ROS 2 (Humble+) +- OpenCV +- FFmpeg/libav +- Boost +- async_web_server_cpp + +### Installing packages + +For newer ROS2 distributions (humble, jazzy, rolling) it is possible to install web_video_server as a package: + +``` +sudo apt install ros-${ROS_DISTRO}-web-video-server +``` + +### Building from Source + +Create a ROS workspace if you don't have one: +```bash +mkdir -p ~/ros_ws/src +cd ~/ros_ws/src +``` + +Clone this repository: +```bash +# ROS 2 +git clone https://github.com/RobotWebTools/web_video_server.git +# ROS 1 +git clone https://github.com/RobotWebTools/web_video_server.git -b ros1 +``` + +Install dependencies with rosdep: +```bash +cd ~/ros_ws +rosdep update +rosdep install --from-paths src -i +``` + +Build the package and source your workspace: +```bash +colcon build --packages-select web_video_server +source install/setup.bash +``` + +## Usage + +### Starting the Server + +```bash +# ROS 1 +rosrun web_video_server web_video_server + +# ROS 2 +ros2 run web_video_server web_video_server +``` + + +### Configuration + +#### Server Configuration Parameters + +| Parameter | Type | Default | Possible Values | Description | +|-----------|------|---------|----------------|-------------| +| `port` | int | 8080 | Any valid port number | HTTP server port | +| `address` | string | "0.0.0.0" | Any valid IP address | HTTP server address (0.0.0.0 allows external connections) | +| `server_threads` | int | 1 | 1+ | Number of server threads for handling HTTP requests | +| `ros_threads` | int | 2 | 1+ | Number of threads for ROS message handling | +| `verbose` | bool | false | true, false | Enable verbose logging | +| `default_stream_type` | string | "mjpeg" | "mjpeg", "vp8", "vp9", "h264", "png", "ros_compressed" | Default format for video streams | +| `publish_rate` | double | -1.0 | -1.0 or positive value | Rate for republishing images (-1.0 means no republishing) | + +#### Running with Custom Parameters + +You can configure the server by passing parameters via the command line: + +```bash +# ROS 1 +rosrun web_video_server web_video_server _port:=8081 _address:=localhost _server_threads:=4 + +# ROS 2 +ros2 run web_video_server web_video_server --ros-args -p port:=8081 -p address:=localhost -p server_threads:=4 +``` + +### View Available Streams +``` +http://localhost:8080/ +``` +The interface allows quick navigation between different topics and formats without having to manually construct URLs. + +This page displays: +- All available streamable ROS topics +- Direct links to view each topic in different formats: + - Web page with streaming image + - Direct stream + - Single image snapshot + +### Stream an Image Topic + +There are two ways to stream the Image, as a HTML page via +``` +http://localhost:8080/stream_viewer?topic=/camera/image_raw +``` +or as a HTTP multipart stream on + +``` +http://localhost:8080/stream?topic=/camera/image_raw +``` +#### URL Parameters for Streaming + +The following parameters can be added to the stream URL: + +| Parameter | Type | Default | Possible Values | Description | +|-----------|------|---------|----------------|-------------| +| `topic` | string | (required) | Any valid ROS image topic | The ROS image topic to stream | +| `type` | string | "mjpeg" | "mjpeg", "vp8", "vp9", "h264", "png", "ros_compressed" | Stream format | +| `width` | int | 0 | 0+ | Width of output stream (0 = original width) | +| `height` | int | 0 | 0+ | Height of output stream (0 = original height) | +| `quality` | int | 95 | 1-100 | Quality for MJPEG and PNG streams | +| `bitrate` | int | 100000 | Positive integer | Bitrate for H264/VP8/VP9 streams in bits/second | +| `invert` | flag | not present | present/not present | Invert image when parameter is present | +| `default_transport` | string | "raw" | "raw", "compressed", "theora" | Image transport to use | +| `qos_profile` | string | "default" | "default", "system_default", "sensor_data", "services_default" | QoS profile for ROS 2 subscribers | + +Examples: + +``` +# Stream an MJPEG at 640x480 with 90% quality +http://localhost:8080/stream?topic=/camera/image_raw&type=mjpeg&width=640&height=480&quality=90 + +# Stream H264 with higher bitrate +http://localhost:8080/stream?topic=/camera/image_raw&type=h264&bitrate=500000 + +# Stream with inverted image (rotated 180°) +http://localhost:8080/stream?topic=/camera/image_raw&invert + +``` + +### Get a Snapshot +It is also possible to get a single image snapshot +``` +http://localhost:8080/snapshot?topic=/camera/image_raw +``` +#### URL Parameters for Snapshot + +| Parameter | Type | Default | Possible Values | Description | +|-----------|------|---------|----------------|-------------| +| `topic` | string | (required) | Any valid ROS image topic | The ROS image topic to stream | +| `type` | string | "jpeg" | "jpeg", "png", "ros_compressed" | Snapshot image format | +| `width` | int | 0 | 0+ | Width of output picture (0 = original width) | +| `height` | int | 0 | 0+ | Height of output picture (0 = original height) | +| `quality` | int | 95 | 1-100 | Quality for JPEG snapshots | +| `invert` | flag | not present | present/not present | Invert image when parameter is present | +| `default_transport` | string | "raw" | "raw", "compressed", "theora" | Image transport to use | +| `qos_profile` | string | "default" | "default", "system_default", "sensor_data", "services_default" | QoS profile for ROS 2 subscribers | + +### Stop an Active Stream + +To stop one or more active streams from the server side (e.g. when a UI component unmounts), use the `/shutdown` endpoint: + +``` +http://localhost:8080/shutdown?topic=/camera/image_raw +``` + +This closes all active streams for the given topic. An optional `client_id` parameter scopes the shutdown to a single named connection: + +``` +http://localhost:8080/shutdown?topic=/camera/image_raw&client_id=my-ui +``` + +To associate a stream with a `client_id`, pass it when opening the stream: + +``` +http://localhost:8080/stream?topic=/camera/image_raw&client_id=my-ui +``` + +The response is plain text in the form `stopped=`, where `` is the number of streams that were stopped. Returns `400 Bad Request` if `topic` is omitted. + +#### URL Parameters for Shutdown + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `topic` | string | (required) | The ROS topic whose streams should be stopped | +| `client_id` | string | (none) | If provided, only the stream with this client_id is stopped | + +## Creating custom streamer plugins +See the [custom streamer plugin tutorial](doc/custom-streamer-plugin.md) for information on how to write your own streamer plugins. + +## About +This project is released as part of the [Robot Web Tools](https://robotwebtools.github.io/) effort. ### License web_video_server is released with a BSD license. For full terms and conditions, see the [LICENSE](LICENSE) file. diff --git a/config-extras.cmake b/config-extras.cmake new file mode 100644 index 00000000..79f31b61 --- /dev/null +++ b/config-extras.cmake @@ -0,0 +1,32 @@ +# Copyright (c) 2025, The Robot Web Tools Contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# ament_export_dependencies fails to propagate Boost components +# so we need to explicitly find Boost here +find_package(Boost REQUIRED COMPONENTS system) diff --git a/doc/custom-streamer-plugin.md b/doc/custom-streamer-plugin.md new file mode 100644 index 00000000..6057c75e --- /dev/null +++ b/doc/custom-streamer-plugin.md @@ -0,0 +1,160 @@ +# How to write a custom streamer plugin + +This tutorial will guide you through the steps to create a simple custom streamer plugin for the `web_video_server` package in ROS 2. The example plugin will log messages when it is created, started, and when frames are restreamed. + +1. Create you local workspace if you don't have one: + ```bash + mkdir -p ~/ros_ws/src + cd ~/ros_ws/src + ``` +1. Create a new package for your custom streamer plugin: + ```bash + ros2 pkg create --build-type ament_cmake test_streamer_plugin --dependencies web_video_server pluginlib --library-name test_streamer_plugin + cd test_streamer_plugin + ``` + +1. Add `TestStreamer` and `TestStreamerFactory` classes to `include/test_streamer_plugin/test_streamer_plugin.hpp` header file: + ```cpp + #ifndef TEST_STREAMER_PLUGIN__TEST_STREAMER_PLUGIN_HPP_ + #define TEST_STREAMER_PLUGIN__TEST_STREAMER_PLUGIN_HPP_ + + #include "test_streamer_plugin/visibility_control.h" + + #include "web_video_server/streamer.hpp" + + namespace test_streamer_plugin + { + + class TestStreamer : public web_video_server::StreamerBase + { + public: + TestStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + + virtual ~TestStreamer(); + + void start() override; + void restream_frame(std::chrono::duration max_age) override; + }; + + class TestStreamerFactory : public web_video_server::StreamerFactoryInterface + { + public: + std::string get_type() override {return "test";} + + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) override; + }; + + } // namespace test_streamer_plugin + + #endif // TEST_STREAMER_PLUGIN__TEST_STREAMER_PLUGIN_HPP_ + ``` + +1. Implement the `TestStreamer` and `TestStreamerFactory` classes in `src/test_streamer_plugin.cpp`: + ```cpp + #include "test_streamer_plugin/test_streamer_plugin.hpp" + + namespace test_streamer_plugin + { + + TestStreamer::TestStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) + : web_video_server::StreamerBase(request, connection, node, "test_streamer") + { + RCLCPP_INFO(logger_, "TestStreamer created for topic: %s", topic_.c_str()); + } + + TestStreamer::~TestStreamer() + { + RCLCPP_INFO(logger_, "TestStreamer destroyed for topic: %s", topic_.c_str()); + } + + void TestStreamer::start() + { + RCLCPP_INFO(logger_, "TestStreamer started for topic: %s", topic_.c_str()); + } + + void TestStreamer::restream_frame(std::chrono::duration max_age) + { + RCLCPP_INFO(logger_, "TestStreamer restream_frame called for topic: %s", topic_.c_str()); + } + + std::shared_ptr TestStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) + { + return std::make_shared(request, connection, node); + } + + } // namespace test_streamer_plugin + + #include "pluginlib/class_list_macros.hpp" + + PLUGINLIB_EXPORT_CLASS( + test_streamer_plugin::TestStreamerFactory, + web_video_server::StreamerFactoryInterface) + ``` + +1. Add `plugins.xml` file with plugin description: + ```xml + + + Test streamer implementation + + + ``` + +1. Update `CMakeLists.txt` to export the plugin description file (Add this anywhere after `find_package` section): + ```cmake + pluginlib_export_plugin_description_file(web_video_server plugins.xml) + ``` + +1. Build your package: + ```bash + cd ~/ros_ws + colcon build --packages-select test_streamer_plugin + source install/setup.bash + ``` + +1. Run the `web_video_server` node and test your custom streamer plugin by accessing a topic with the `test` format: + ```bash + ros2 run web_video_server web_video_server + ``` + Then open your web browser and navigate to: + ``` + http://localhost:8080/stream?topic=/your_image_topic&format=test + ``` + +## Implementation hints +- You can access query parameters from the HTTP request in your streamer constructor using `request.get_query_param_value_or_default` method. +- Use `logger_` member variable from the base `StreamerBase` class for logging. +- Inherit from `web_video_server::streamers::ImageTransportStreamerBase` instead of `web_video_server::StreamerBase` if you want to use image transport functionality without writing boilerplate code. +- By default, in the topic list view, your custom streamer will not be shown for any topic. To change it, overwrite `get_available_topics` method in your `StreamerFactory` class or inherit from `ImageTransportStreamerFactoryBase` to make your format available for all topics using `sensor_msgs/msg/Image` message type. +- Link specific targets in `CMakeLists.txt`. For example, replace: + ```cmake + target_link_libraries( + test_streamer_plugin PUBLIC + ${web_video_server_TARGETS} + ${pluginlib_TARGETS} + ) + ``` + with: + ```cmake + target_link_libraries( + test_streamer_plugin + web_video_server::web_video_server + pluginlib::pluginlib + ) + ``` + Add `web_video_server::web_video_server_streamers` if you inherit from `ImageTransportStreamerBase`. + diff --git a/include/web_video_server/h264_streamer.h b/include/web_video_server/h264_streamer.h deleted file mode 100644 index 9961389a..00000000 --- a/include/web_video_server/h264_streamer.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef H264_STREAMERS_H_ -#define H264_STREAMERS_H_ - -#include -#include "web_video_server/libav_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" - -namespace web_video_server -{ - -class H264Streamer : public LibavStreamer -{ -public: - H264Streamer(const async_web_server_cpp::HttpRequest& request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~H264Streamer(); -protected: - virtual void initializeEncoder(); - std::string preset_; -}; - -class H264StreamerType : public LibavStreamerType -{ -public: - H264StreamerType(); - virtual boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); -}; - -} - -#endif - diff --git a/include/web_video_server/image_streamer.h b/include/web_video_server/image_streamer.h deleted file mode 100644 index 59a1a928..00000000 --- a/include/web_video_server/image_streamer.h +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef IMAGE_STREAMER_H_ -#define IMAGE_STREAMER_H_ - -#include -#include -#include -#include -#include "async_web_server_cpp/http_server.hpp" -#include "async_web_server_cpp/http_request.hpp" - -namespace web_video_server -{ - -class ImageStreamer -{ -public: - ImageStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - - virtual void start() = 0; - virtual ~ImageStreamer(); - - bool isInactive() - { - return inactive_; - } - ; - - /** - * Restreams the last received image frame if older than max_age. - */ - virtual void restreamFrame(double max_age) = 0; - - std::string getTopic() - { - return topic_; - } - ; -protected: - async_web_server_cpp::HttpConnectionPtr connection_; - async_web_server_cpp::HttpRequest request_; - rclcpp::Node::SharedPtr nh_; - bool inactive_; - image_transport::Subscriber image_sub_; - std::string topic_; -}; - - -class ImageTransportImageStreamer : public ImageStreamer -{ -public: - ImageTransportImageStreamer(const async_web_server_cpp::HttpRequest &request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - virtual ~ImageTransportImageStreamer(); - - virtual void start(); - -protected: - virtual void sendImage(const cv::Mat &, const rclcpp::Time &time) = 0; - virtual void restreamFrame(double max_age); - virtual void initialize(const cv::Mat &); - - image_transport::Subscriber image_sub_; - int output_width_; - int output_height_; - bool invert_; - std::string default_transport_; - - rclcpp::Time last_frame; - cv::Mat output_size_image; - boost::mutex send_mutex_; - -private: - image_transport::ImageTransport it_; - bool initialized_; - - void imageCallback(const sensor_msgs::msg::Image::ConstSharedPtr &msg); -}; - -class ImageStreamerType -{ -public: - virtual boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) = 0; - - virtual std::string create_viewer(const async_web_server_cpp::HttpRequest &request) = 0; -}; - -} - -#endif diff --git a/include/web_video_server/jpeg_streamers.h b/include/web_video_server/jpeg_streamers.h deleted file mode 100644 index ad788fa7..00000000 --- a/include/web_video_server/jpeg_streamers.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef JPEG_STREAMERS_H_ -#define JPEG_STREAMERS_H_ - -#include -#include "web_video_server/image_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" -#include "web_video_server/multipart_stream.h" - -namespace web_video_server -{ - -class MjpegStreamer : public ImageTransportImageStreamer -{ -public: - MjpegStreamer(const async_web_server_cpp::HttpRequest &request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~MjpegStreamer(); -protected: - virtual void sendImage(const cv::Mat &, const rclcpp::Time &time); - -private: - MultipartStream stream_; - int quality_; -}; - -class MjpegStreamerType : public ImageStreamerType -{ -public: - boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - std::string create_viewer(const async_web_server_cpp::HttpRequest &request); -}; - -class JpegSnapshotStreamer : public ImageTransportImageStreamer -{ -public: - JpegSnapshotStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh); - ~JpegSnapshotStreamer(); -protected: - virtual void sendImage(const cv::Mat &, const rclcpp::Time &time); - -private: - int quality_; -}; - -} - -#endif diff --git a/include/web_video_server/libav_streamer.h b/include/web_video_server/libav_streamer.h deleted file mode 100644 index 643e3b33..00000000 --- a/include/web_video_server/libav_streamer.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef LIBAV_STREAMERS_H_ -#define LIBAV_STREAMERS_H_ - -#include -#include "web_video_server/image_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" - -extern "C" -{ -#include -#include -#include -#include -#include -#include -#include -#include -} - -namespace web_video_server -{ - -class LibavStreamer : public ImageTransportImageStreamer -{ -public: - LibavStreamer(const async_web_server_cpp::HttpRequest &request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh, const std::string &format_name, const std::string &codec_name, - const std::string &content_type); - - ~LibavStreamer(); - -protected: - virtual void initializeEncoder(); - virtual void sendImage(const cv::Mat&, const rclcpp::Time& time); - virtual void initialize(const cv::Mat&); - AVOutputFormat* output_format_; - AVFormatContext* format_context_; - AVCodec* codec_; - AVCodecContext* codec_context_; - AVStream* video_stream_; - - AVDictionary* opt_; // container format options - -private: - AVFrame* frame_; - struct SwsContext* sws_context_; - rclcpp::Time first_image_timestamp_; - boost::mutex encode_mutex_; - - std::string format_name_; - std::string codec_name_; - std::string content_type_; - int bitrate_; - int qmin_; - int qmax_; - int gop_; - - uint8_t* io_buffer_; // custom IO buffer -}; - -class LibavStreamerType : public ImageStreamerType -{ -public: - LibavStreamerType(const std::string &format_name, const std::string &codec_name, const std::string &content_type); - - boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - - std::string create_viewer(const async_web_server_cpp::HttpRequest &request); - -private: - const std::string format_name_; - const std::string codec_name_; - const std::string content_type_; -}; - -} - -#endif diff --git a/include/web_video_server/multipart_stream.h b/include/web_video_server/multipart_stream.h deleted file mode 100644 index d6ea3592..00000000 --- a/include/web_video_server/multipart_stream.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef MULTIPART_STREAM_H_ -#define MULTIPART_STREAM_H_ - -#include -#include - -#include - -namespace web_video_server -{ - -struct PendingFooter { - rclcpp::Time timestamp; - std::weak_ptr contents; -}; - -class MultipartStream { -public: - MultipartStream(std::function get_now, - async_web_server_cpp::HttpConnectionPtr& connection, - const std::string& boundry="boundarydonotcross", - std::size_t max_queue_size=1); - - void sendInitialHeader(); - void sendPartHeader(const rclcpp::Time &time, const std::string& type, size_t payload_size); - void sendPartFooter(const rclcpp::Time &time); - void sendPartAndClear(const rclcpp::Time &time, const std::string& type, std::vector &data); - void sendPart(const rclcpp::Time &time, const std::string& type, const boost::asio::const_buffer &buffer, - async_web_server_cpp::HttpConnection::ResourcePtr resource); - -private: - bool isBusy(); - -private: - std::function get_now_; - const std::size_t max_queue_size_; - async_web_server_cpp::HttpConnectionPtr connection_; - std::string boundry_; - std::queue pending_footers_; -}; - -} - -#endif diff --git a/include/web_video_server/multipart_stream.hpp b/include/web_video_server/multipart_stream.hpp new file mode 100644 index 00000000..26edfce0 --- /dev/null +++ b/include/web_video_server/multipart_stream.hpp @@ -0,0 +1,86 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_connection.hpp" + +namespace web_video_server +{ + +struct PendingFooter +{ + std::chrono::steady_clock::time_point timestamp; + std::weak_ptr contents; +}; + +/** + * Helper class to manage sending multipart HTTP responses. + */ +class MultipartStream +{ +public: + explicit MultipartStream( + async_web_server_cpp::HttpConnectionPtr & connection, + const std::string & boundary = "boundarydonotcross", + std::size_t max_queue_size = 1); + + void send_initial_header(); + void send_part_header( + const std::chrono::steady_clock::time_point & time, const std::string & type, + size_t payload_size); + void send_part_footer(const std::chrono::steady_clock::time_point & time); + void send_part_and_clear( + const std::chrono::steady_clock::time_point & time, const std::string & type, + std::vector & data); + void send_part( + const std::chrono::steady_clock::time_point & time, const std::string & type, + const boost::asio::const_buffer & buffer, + async_web_server_cpp::HttpConnection::ResourcePtr resource); + +private: + bool is_busy(); + + const std::size_t max_queue_size_; + async_web_server_cpp::HttpConnectionPtr connection_; + std::string boundary_; + std::queue pending_footers_; +}; + +} // namespace web_video_server diff --git a/include/web_video_server/png_streamers.h b/include/web_video_server/png_streamers.h deleted file mode 100644 index a6edabc1..00000000 --- a/include/web_video_server/png_streamers.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef PNG_STREAMERS_H_ -#define PNG_STREAMERS_H_ - -#include -#include "web_video_server/image_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" -#include "web_video_server/multipart_stream.h" - -namespace web_video_server -{ - -class PngStreamer : public ImageTransportImageStreamer -{ -public: - PngStreamer(const async_web_server_cpp::HttpRequest &request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~PngStreamer(); -protected: - virtual void sendImage(const cv::Mat &, const rclcpp::Time &time); - -private: - MultipartStream stream_; - int quality_; -}; - -class PngStreamerType : public ImageStreamerType -{ -public: - boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - std::string create_viewer(const async_web_server_cpp::HttpRequest &request); -}; - -class PngSnapshotStreamer : public ImageTransportImageStreamer -{ -public: - PngSnapshotStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh); - ~PngSnapshotStreamer(); -protected: - virtual void sendImage(const cv::Mat &, const rclcpp::Time &time); - -private: - int quality_; -}; - -} - -#endif diff --git a/include/web_video_server/ros_compressed_streamer.h b/include/web_video_server/ros_compressed_streamer.h deleted file mode 100644 index 7219dfdb..00000000 --- a/include/web_video_server/ros_compressed_streamer.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef ROS_COMPRESSED_STREAMERS_H_ -#define ROS_COMPRESSED_STREAMERS_H_ - -#include -#include "web_video_server/image_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" -#include "web_video_server/multipart_stream.h" - -namespace web_video_server -{ - -class RosCompressedStreamer : public ImageStreamer -{ -public: - RosCompressedStreamer(const async_web_server_cpp::HttpRequest &request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~RosCompressedStreamer(); - virtual void start(); - virtual void restreamFrame(double max_age); - -protected: - virtual void sendImage(const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, const rclcpp::Time &time); - -private: - void imageCallback(const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg); - MultipartStream stream_; - rclcpp::Subscription::SharedPtr image_sub_; - rclcpp::Time last_frame; - sensor_msgs::msg::CompressedImage::ConstSharedPtr last_msg; - boost::mutex send_mutex_; -}; - -class RosCompressedStreamerType : public ImageStreamerType -{ -public: - boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - std::string create_viewer(const async_web_server_cpp::HttpRequest &request); -}; - -} - -#endif diff --git a/include/web_video_server/streamer.hpp b/include/web_video_server/streamer.hpp new file mode 100644 index 00000000..bdb13b7f --- /dev/null +++ b/include/web_video_server/streamer.hpp @@ -0,0 +1,179 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/logger.hpp" +#include "rclcpp/node.hpp" + +namespace web_video_server +{ + +/** + * @brief A common interface for all streaming plugins. + */ +class StreamerInterface +{ +public: + virtual ~StreamerInterface() {} + + /** + * @brief Starts the streaming process. + */ + virtual void start() = 0; + + /** + * @brief Stops the streaming process and marks the streamer as inactive. + */ + virtual void stop() = 0; + + /** + * @brief Returns true if the streamer is inactive and should be deleted. + * + * This could be because the connection was closed or snapshot was successfully sent (in case + * of snapshot streamers). + */ + virtual bool is_inactive() = 0; + + /** + * @brief Restreams the last received image frame if older than max_age. + */ + virtual void restream_frame(std::chrono::duration max_age) = 0; + + /** + * @brief Returns the topic being streamed. + */ + virtual std::string get_topic() = 0; + + /** + * @brief Returns the client_id associated with this stream, or an empty string if none. + */ + virtual std::string get_client_id() = 0; +}; + +/** + * @brief A base class providing common functionality for streamers. + */ +class StreamerBase : public StreamerInterface +{ +public: + StreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node, + std::string logger_name = "streamer"); + + void stop() override + { + inactive_ = true; + connection_.reset(); + } + + bool is_inactive() override + { + return inactive_; + } + + std::string get_topic() override + { + return topic_; + } + + std::string get_client_id() override + { + return client_id_; + } + +protected: + rclcpp::Node::SharedPtr lock_node() const; + + async_web_server_cpp::HttpConnectionPtr connection_; + async_web_server_cpp::HttpRequest request_; + rclcpp::Node::WeakPtr node_; + rclcpp::Logger logger_; + bool inactive_; + std::string topic_; + std::string client_id_; +}; + +/** + * @brief A factory interface for creating Streamer instances. + */ +class StreamerFactoryInterface +{ +public: + virtual ~StreamerFactoryInterface() = default; + + /** + * @brief Returns the type of streamer created by this factory. + * + * This should match the "type" query parameter used to select the streamer. + */ + virtual std::string get_type() = 0; + + /** + * @brief Creates a new Streamer instance. + * @param request The HTTP request that initiated the streamer. + * @param connection The HTTP connection to use for streaming. + * @param node The ROS2 node to use for subscribing to topics. + * @return A shared pointer to the created Streamer instance. + */ + virtual std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) = 0; + + /** + * @brief Creates HTML code for embedding a viewer for this streamer. + * @param request The HTTP request that initiated the viewer. + */ + virtual std::string create_viewer(const async_web_server_cpp::HttpRequest & request); + + /** + * @brief Returns a list of available topics that can be streamed by this streamer. + * @param node The ROS2 node to use for discovering topics. + * @return A vector of topic names. + */ + virtual std::vector get_available_topics(rclcpp::Node & node); +}; + +/** + * @brief A factory interface for creating snapshot Streamer instances. + */ +class SnapshotStreamerFactoryInterface : public StreamerFactoryInterface {}; + +} // namespace web_video_server diff --git a/include/web_video_server/streamers/h264_streamer.hpp b/include/web_video_server/streamers/h264_streamer.hpp new file mode 100644 index 00000000..e6cc0889 --- /dev/null +++ b/include/web_video_server/streamers/h264_streamer.hpp @@ -0,0 +1,72 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "async_web_server_cpp/http_request.hpp" +#include "async_web_server_cpp/http_connection.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class H264Streamer : public LibavStreamerBase +{ +public: + H264Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~H264Streamer(); + +protected: + virtual void initialize_encoder(); + std::string preset_; +}; + +class H264StreamerFactory : public LibavStreamerFactoryBase +{ +public: + std::string get_type() {return "h264";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/image_transport_streamer.hpp b/include/web_video_server/streamers/image_transport_streamer.hpp new file mode 100644 index 00000000..141c6d7e --- /dev/null +++ b/include/web_video_server/streamers/image_transport_streamer.hpp @@ -0,0 +1,111 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "image_transport/image_transport.hpp" +#include "image_transport/subscriber.hpp" +#include "rclcpp/node.hpp" +#include "sensor_msgs/msg/image.hpp" + +#include "web_video_server/streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +/** + * @brief A common base class for all streaming plugins using image_transport to subscribe to image + * topics. + */ +class ImageTransportStreamerBase : public StreamerBase +{ +public: + ImageTransportStreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node, + std::string logger_name = "image_transport_streamer"); + virtual ~ImageTransportStreamerBase(); + + virtual void start(); + virtual void restream_frame(std::chrono::duration max_age); + +protected: + virtual cv::Mat decode_image(const sensor_msgs::msg::Image::ConstSharedPtr & msg); + virtual void send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) = 0; + virtual void initialize(const cv::Mat & img); + + image_transport::Subscriber image_sub_; + int output_width_; + int output_height_; + bool invert_; + std::string default_transport_; + std::string qos_profile_name_; + + std::chrono::steady_clock::time_point last_frame_; + cv::Mat output_size_image_; + std::mutex send_mutex_; + +private: + bool initialized_; + + void image_callback(const sensor_msgs::msg::Image::ConstSharedPtr & msg); + void try_send_image( + const cv::Mat & img, const std::chrono::steady_clock::time_point & time, + rclcpp::Node & node); +}; + +class ImageTransportStreamerFactoryBase : public StreamerFactoryInterface +{ +public: + virtual std::vector get_available_topics(rclcpp::Node & node); +}; + +class ImageTransportSnapshotStreamerFactoryBase : public SnapshotStreamerFactoryInterface +{ +public: + virtual std::vector get_available_topics(rclcpp::Node & node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/jpeg_streamers.hpp b/include/web_video_server/streamers/jpeg_streamers.hpp new file mode 100644 index 00000000..5b9e1ba4 --- /dev/null +++ b/include/web_video_server/streamers/jpeg_streamers.hpp @@ -0,0 +1,107 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_request.hpp" +#include "async_web_server_cpp/http_connection.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/multipart_stream.hpp" +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/image_transport_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class MjpegStreamer : public ImageTransportStreamerBase +{ +public: + MjpegStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~MjpegStreamer(); + +protected: + virtual void send_image(const cv::Mat & img, const std::chrono::steady_clock::time_point & time); + +private: + MultipartStream stream_; + int quality_; +}; + +class MjpegStreamerFactory : public ImageTransportStreamerFactoryBase +{ +public: + std::string get_type() {return "mjpeg";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +class JpegSnapshotStreamer : public ImageTransportStreamerBase +{ +public: + JpegSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~JpegSnapshotStreamer(); + +protected: + virtual void send_image(const cv::Mat & img, const std::chrono::steady_clock::time_point & time); + +private: + int quality_; +}; + +class JpegSnapshotStreamerFactory : public ImageTransportSnapshotStreamerFactoryBase +{ +public: + std::string get_type() {return "jpeg";} + + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/libav_streamer.hpp b/include/web_video_server/streamers/libav_streamer.hpp new file mode 100644 index 00000000..02b23e82 --- /dev/null +++ b/include/web_video_server/streamers/libav_streamer.hpp @@ -0,0 +1,115 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +extern "C" +{ +#include +#include +#include +#include +#include +#include +} + +#include +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamers/image_transport_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +/** + * @brief A common base class for all streaming plugins using image_transport to subscribe to image + * topics and libav to encode and stream video. + */ +class LibavStreamerBase : public ImageTransportStreamerBase +{ +public: + LibavStreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node, + std::string logger_name, + const std::string & format_name, + const std::string & codec_name, + const std::string & content_type); + + ~LibavStreamerBase(); + +protected: + virtual void initialize_encoder() = 0; + virtual void send_image(const cv::Mat & img, const std::chrono::steady_clock::time_point & time); + virtual void initialize(const cv::Mat & img); + AVFormatContext * format_context_; + const AVCodec * codec_; + AVCodecContext * codec_context_; + AVStream * video_stream_; + + AVDictionary * opt_; // container format options + +private: + AVFrame * frame_; + struct SwsContext * sws_context_; + std::mutex encode_mutex_; + bool first_image_received_; + std::chrono::steady_clock::time_point first_image_time_; + + std::string format_name_; + std::string codec_name_; + std::string content_type_; + int bitrate_; + int qmin_; + int qmax_; + int gop_; + + uint8_t * io_buffer_; // custom IO buffer +}; + +class LibavStreamerFactoryBase : public ImageTransportStreamerFactoryBase +{ +public: + virtual std::string create_viewer(const async_web_server_cpp::HttpRequest & request); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/png_streamers.hpp b/include/web_video_server/streamers/png_streamers.hpp new file mode 100644 index 00000000..ef2cde43 --- /dev/null +++ b/include/web_video_server/streamers/png_streamers.hpp @@ -0,0 +1,109 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_request.hpp" +#include "async_web_server_cpp/http_connection.hpp" +#include "rclcpp/node.hpp" +#include "sensor_msgs/msg/image.hpp" + +#include "web_video_server/multipart_stream.hpp" +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/image_transport_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class PngStreamer : public ImageTransportStreamerBase +{ +public: + PngStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~PngStreamer(); + +protected: + virtual void send_image(const cv::Mat & img, const std::chrono::steady_clock::time_point & time); + virtual cv::Mat decode_image(const sensor_msgs::msg::Image::ConstSharedPtr & msg); + +private: + MultipartStream stream_; + int quality_; +}; + +class PngStreamerFactory : public ImageTransportStreamerFactoryBase +{ +public: + std::string get_type() {return "png";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +class PngSnapshotStreamer : public ImageTransportStreamerBase +{ +public: + PngSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~PngSnapshotStreamer(); + +protected: + virtual void send_image(const cv::Mat & img, const std::chrono::steady_clock::time_point & time); + virtual cv::Mat decode_image(const sensor_msgs::msg::Image::ConstSharedPtr & msg); + +private: + int quality_; +}; + +class PngSnapshotStreamerFactory : public ImageTransportSnapshotStreamerFactoryBase +{ +public: + std::string get_type() {return "png";} + + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/ros_compressed_streamer.hpp b/include/web_video_server/streamers/ros_compressed_streamer.hpp new file mode 100644 index 00000000..9b20b96c --- /dev/null +++ b/include/web_video_server/streamers/ros_compressed_streamer.hpp @@ -0,0 +1,125 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/subscription.hpp" +#include "sensor_msgs/msg/compressed_image.hpp" + +#include "web_video_server/multipart_stream.hpp" +#include "web_video_server/streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class RosCompressedStreamer : public StreamerBase +{ +public: + RosCompressedStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~RosCompressedStreamer(); + virtual void start(); + virtual void restream_frame(std::chrono::duration max_age); + +protected: + virtual void send_image( + sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, + const std::chrono::steady_clock::time_point & time); + +private: + void image_callback(sensor_msgs::msg::CompressedImage::ConstSharedPtr msg); + MultipartStream stream_; + rclcpp::Subscription::SharedPtr image_sub_; + std::chrono::steady_clock::time_point last_frame_; + sensor_msgs::msg::CompressedImage::ConstSharedPtr last_msg_; + std::mutex send_mutex_; + std::string qos_profile_name_; +}; + +class RosCompressedStreamerFactory : public StreamerFactoryInterface +{ +public: + std::string get_type() {return "ros_compressed";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + std::vector get_available_topics(rclcpp::Node & node); +}; + +class RosCompressedSnapshotStreamer : public StreamerBase +{ +public: + RosCompressedSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~RosCompressedSnapshotStreamer(); + virtual void start(); + virtual void restream_frame(std::chrono::duration max_age); + +protected: + virtual void send_image( + sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, + const std::chrono::steady_clock::time_point & time); + +private: + void image_callback(sensor_msgs::msg::CompressedImage::ConstSharedPtr msg); + + rclcpp::Subscription::SharedPtr image_sub_; + std::string qos_profile_name_; +}; + +class RosCompressedSnapshotStreamerFactory : public SnapshotStreamerFactoryInterface +{ +public: + std::string get_type() {return "ros_compressed";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + std::vector get_available_topics(rclcpp::Node & node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/vp8_streamer.hpp b/include/web_video_server/streamers/vp8_streamer.hpp new file mode 100644 index 00000000..aeee06a0 --- /dev/null +++ b/include/web_video_server/streamers/vp8_streamer.hpp @@ -0,0 +1,75 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class Vp8Streamer : public LibavStreamerBase +{ +public: + Vp8Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~Vp8Streamer(); + +protected: + virtual void initialize_encoder(); + +private: + std::string quality_; +}; + +class Vp8StreamerFactory : public LibavStreamerFactoryBase +{ +public: + std::string get_type() {return "vp8";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/streamers/vp9_streamer.hpp b/include/web_video_server/streamers/vp9_streamer.hpp new file mode 100644 index 00000000..97d5e734 --- /dev/null +++ b/include/web_video_server/streamers/vp9_streamer.hpp @@ -0,0 +1,71 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +class Vp9Streamer : public LibavStreamerBase +{ +public: + Vp9Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); + ~Vp9Streamer(); + +protected: + virtual void initialize_encoder(); +}; + +class Vp9StreamerFactory : public LibavStreamerFactoryBase +{ +public: + std::string get_type() {return "vp9";} + std::shared_ptr create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node); +}; + +} // namespace streamers +} // namespace web_video_server diff --git a/include/web_video_server/utils.hpp b/include/web_video_server/utils.hpp new file mode 100644 index 00000000..1549e19c --- /dev/null +++ b/include/web_video_server/utils.hpp @@ -0,0 +1,47 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "rmw/types.h" + +namespace web_video_server +{ + +/** + * @brief Gets a QoS profile given an input name, if valid. + * @param name The name of the QoS profile name. + * @return An optional containing the matching QoS profile. + */ +std::optional get_qos_profile_from_name(std::string name); + +} // namespace web_video_server diff --git a/include/web_video_server/vp8_streamer.h b/include/web_video_server/vp8_streamer.h deleted file mode 100644 index 46e8bed4..00000000 --- a/include/web_video_server/vp8_streamer.h +++ /dev/null @@ -1,72 +0,0 @@ -/********************************************************************* - * - * Software License Agreement (BSD License) - * - * Copyright (c) 2014, Worcester Polytechnic Institute - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the Worcester Polytechnic Institute nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - *********************************************************************/ - -#ifndef VP8_STREAMERS_H_ -#define VP8_STREAMERS_H_ - -#include -#include "web_video_server/libav_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" - -namespace web_video_server -{ - -class Vp8Streamer : public LibavStreamer -{ -public: - Vp8Streamer(const async_web_server_cpp::HttpRequest& request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~Vp8Streamer(); -protected: - virtual void initializeEncoder(); -private: - std::string quality_; -}; - -class Vp8StreamerType : public LibavStreamerType -{ -public: - Vp8StreamerType(); - virtual boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); -}; - -} - -#endif - diff --git a/include/web_video_server/vp9_streamer.h b/include/web_video_server/vp9_streamer.h deleted file mode 100644 index 06c48f85..00000000 --- a/include/web_video_server/vp9_streamer.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef VP9_STREAMERS_H_ -#define VP9_STREAMERS_H_ - -#include -#include "web_video_server/libav_streamer.h" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" - -namespace web_video_server -{ - -class Vp9Streamer : public LibavStreamer -{ -public: - Vp9Streamer(const async_web_server_cpp::HttpRequest& request, async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); - ~Vp9Streamer(); -protected: - virtual void initializeEncoder(); -}; - -class Vp9StreamerType : public LibavStreamerType -{ -public: - Vp9StreamerType(); - virtual boost::shared_ptr create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh); -}; - -} - -#endif diff --git a/include/web_video_server/web_video_server.h b/include/web_video_server/web_video_server.h deleted file mode 100644 index 1da6015f..00000000 --- a/include/web_video_server/web_video_server.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef WEB_VIDEO_SERVER_H_ -#define WEB_VIDEO_SERVER_H_ - -#include -#include -#include -#include "web_video_server/image_streamer.h" -#include "async_web_server_cpp/http_server.hpp" -#include "async_web_server_cpp/http_request.hpp" -#include "async_web_server_cpp/http_connection.hpp" - -namespace web_video_server -{ - -/** - * @class WebVideoServer - * @brief - */ -class WebVideoServer -{ -public: - /** - * @brief Constructor - * @return - */ - WebVideoServer(rclcpp::Node::SharedPtr &nh, rclcpp::Node::SharedPtr &private_nh); - - /** - * @brief Destructor - Cleans up - */ - virtual ~WebVideoServer(); - - /** - * @brief Starts the server and spins - */ - void spin(); - - void setup_cleanup_inactive_streams(); - - bool handle_stream(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, const char* end); - - bool handle_stream_viewer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, const char* end); - - bool handle_snapshot(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, const char* end); - - bool handle_list_streams(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, const char* end); - -private: - void restreamFrames(double max_age); - void cleanup_inactive_streams(); - - rclcpp::Node::SharedPtr nh_; - rclcpp::WallTimer::SharedPtr cleanup_timer_; - int ros_threads_; - double publish_rate_; - int port_; - std::string address_; - boost::shared_ptr server_; - async_web_server_cpp::HttpRequestHandlerGroup handler_group_; - - std::vector > image_subscribers_; - std::map > stream_types_; - boost::mutex subscriber_mutex_; -}; - -} - -#endif diff --git a/include/web_video_server/web_video_server.hpp b/include/web_video_server/web_video_server.hpp new file mode 100644 index 00000000..3029929e --- /dev/null +++ b/include/web_video_server/web_video_server.hpp @@ -0,0 +1,128 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "async_web_server_cpp/http_request_handler.hpp" +#include "async_web_server_cpp/http_server.hpp" +#include "pluginlib/class_loader.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/node_options.hpp" +#include "rclcpp/timer.hpp" + +#include "web_video_server/streamer.hpp" + +namespace web_video_server +{ + +/** + * @class WebVideoServer + * @brief + */ +class WebVideoServer : public rclcpp::Node +{ +public: + /** + * @brief Constructor + * @return + */ + explicit WebVideoServer(const rclcpp::NodeOptions & options); + + /** + * @brief Destructor - Cleans up + */ + virtual ~WebVideoServer(); + + bool handle_request( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + + bool handle_stream( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + + bool handle_stream_viewer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + + bool handle_snapshot( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + + bool handle_shutdown( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + + bool handle_list_streams( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + const char * begin, const char * end); + +private: + void restream_frames(std::chrono::duration max_age); + void cleanup_inactive_streams(); + + rclcpp::TimerBase::SharedPtr restream_timer_; + rclcpp::TimerBase::SharedPtr cleanup_timer_; + + // Parameters + double publish_rate_; + int port_; + std::string address_; + bool verbose_; + std::string default_stream_type_; + std::string default_snapshot_type_; + + std::shared_ptr server_; + async_web_server_cpp::HttpRequestHandlerGroup handler_group_; + + std::vector> streamers_; + pluginlib::ClassLoader streamer_factory_loader_; + std::map> streamer_factories_; + pluginlib::ClassLoader snapshot_streamer_factory_loader_; + std::map> snapshot_streamer_factories_; + std::mutex streamers_mutex_; +}; + +} // namespace web_video_server diff --git a/package.xml b/package.xml index 6e3a11cc..a4dba071 100644 --- a/package.xml +++ b/package.xml @@ -1,11 +1,11 @@ - + + web_video_server - 1.0.0 + 3.0.0 HTTP Streaming of ROS Image Topics in Multiple Formats - Russell Toris - Mitchell Wills + Błażej Sowa BSD @@ -13,21 +13,35 @@ https://github.com/RobotWebTools/web_video_server/issues https://github.com/RobotWebTools/web_video_server + Mitchell Wills + ament_cmake_ros + pkg-config + + + async_web_server_cpp + boost + cv_bridge + ffmpeg + image_transport + libopencv-dev + rclcpp + rmw + sensor_msgs + + + pluginlib + pluginlib + rclcpp_components + rclcpp_components - rclcpp - cv_bridge - image_transport - async_web_server_cpp - ffmpeg - sensor_msgs - - rclcpp - cv_bridge - image_transport - async_web_server_cpp - ffmpeg - sensor_msgs + ament_lint_auto + ament_cmake_clang_tidy + ament_cmake_copyright + ament_cmake_lint_cmake + ament_cmake_xmllint + ament_cmake_uncrustify + ros_environment ament_cmake diff --git a/plugins.xml b/plugins.xml new file mode 100644 index 00000000..836afda2 --- /dev/null +++ b/plugins.xml @@ -0,0 +1,48 @@ + + + Streams images as multipart/x-mixed-replace (MJPEG). + + + Streams images as multipart/x-mixed-replace (PNG). + + + Streams images published using compressed_image_transport as + multipart/x-mixed-replace. + + + Streams images as H.264 encoded video in an MP4 container. + + + Streams images as VP8 encoded video in a WebM container. + + + Streams images as VP9 encoded video in a WebM container. + + + Provides single JPEG images. + + + Provides single PNG images. + + + Provides single images published using compressed_image_transport as JPEG or PNG. + + \ No newline at end of file diff --git a/src/h264_streamer.cpp b/src/h264_streamer.cpp deleted file mode 100644 index a866aa12..00000000 --- a/src/h264_streamer.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "web_video_server/h264_streamer.h" - -namespace web_video_server -{ - -H264Streamer::H264Streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - LibavStreamer(request, connection, nh, "mp4", "libx264", "video/mp4") -{ - /* possible quality presets: - * ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo - * no latency improvements observed with ultrafast instead of medium - */ - preset_ = request.get_query_param_value_or_default("preset", "ultrafast"); -} - -H264Streamer::~H264Streamer() -{ -} - -void H264Streamer::initializeEncoder() -{ - av_opt_set(codec_context_->priv_data, "preset", preset_.c_str(), 0); - av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0); - av_opt_set_int(codec_context_->priv_data, "crf", 20, 0); - av_opt_set_int(codec_context_->priv_data, "bufsize", 100, 0); - av_opt_set_int(codec_context_->priv_data, "keyint", 30, 0); - av_opt_set_int(codec_context_->priv_data, "g", 1, 0); - - // container format options - if (!strcmp(format_context_->oformat->name, "mp4")) { - // set up mp4 for streaming (instead of seekable file output) - av_dict_set(&opt_, "movflags", "+frag_keyframe+empty_moov+faststart", 0); - } -} - -H264StreamerType::H264StreamerType() : - LibavStreamerType("mp4", "libx264", "video/mp4") -{ -} - -boost::shared_ptr H264StreamerType::create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new H264Streamer(request, connection, nh)); -} - -} diff --git a/src/image_streamer.cpp b/src/image_streamer.cpp deleted file mode 100644 index 05012c20..00000000 --- a/src/image_streamer.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "web_video_server/image_streamer.h" -#include -#include - -namespace web_video_server -{ - -ImageStreamer::ImageStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - request_(request), connection_(connection), nh_(nh), inactive_(false) -{ - topic_ = request.get_query_param_value_or_default("topic", ""); -} - -ImageStreamer::~ImageStreamer() -{ -} - -ImageTransportImageStreamer::ImageTransportImageStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - ImageStreamer(request, connection, nh), it_(nh), initialized_(false) -{ - output_width_ = request.get_query_param_value_or_default("width", -1); - output_height_ = request.get_query_param_value_or_default("height", -1); - invert_ = request.has_query_param("invert"); - default_transport_ = request.get_query_param_value_or_default("default_transport", "raw"); -} - -ImageTransportImageStreamer::~ImageTransportImageStreamer() -{ -} - -void ImageTransportImageStreamer::start() -{ - image_transport::TransportHints hints(nh_.get(), default_transport_); - auto tnat = nh_->get_topic_names_and_types(); - inactive_ = true; - for (auto topic_and_types : tnat) { - if (topic_and_types.second.size() > 1) { - // explicitly avoid topics with more than one type - break; - } - auto & topic_name = topic_and_types.first; - if(topic_name == topic_ || (topic_name.find("/") == 0 && topic_name.substr(1) == topic_)){ - inactive_ = false; - break; - } - } - image_sub_ = it_.subscribe(topic_, 1, &ImageTransportImageStreamer::imageCallback, this, &hints); -} - -void ImageTransportImageStreamer::initialize(const cv::Mat &) -{ -} - -void ImageTransportImageStreamer::restreamFrame(double max_age) -{ - if (inactive_ || !initialized_ ) - return; - try { - if ( last_frame + rclcpp::Duration::from_seconds(max_age) < nh_->now() ) { - boost::mutex::scoped_lock lock(send_mutex_); - sendImage(output_size_image, nh_->now() ); // don't update last_frame, it may remain an old value. - } - } - catch (boost::system::system_error &e) - { - // happens when client disconnects - RCLCPP_DEBUG(nh_->get_logger(), "system_error exception: %s", e.what()); - inactive_ = true; - return; - } - catch (std::exception &e) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception: %s", e.what()); - inactive_ = true; - return; - } - catch (...) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception"); - inactive_ = true; - return; - } -} - -void ImageTransportImageStreamer::imageCallback(const sensor_msgs::msg::Image::ConstSharedPtr &msg) -{ - if (inactive_) - return; - - cv::Mat img; - try - { - if (msg->encoding.find("F") != std::string::npos) - { - // scale floating point images - cv::Mat float_image_bridge = cv_bridge::toCvCopy(msg, msg->encoding)->image; - cv::Mat_ float_image = float_image_bridge; - double max_val; - cv::minMaxIdx(float_image, 0, &max_val); - - if (max_val > 0) - { - float_image *= (255 / max_val); - } - img = float_image; - } - else - { - // Convert to OpenCV native BGR color - img = cv_bridge::toCvCopy(msg, "bgr8")->image; - } - - int input_width = img.cols; - int input_height = img.rows; - - if (output_width_ == -1) - output_width_ = input_width; - if (output_height_ == -1) - output_height_ = input_height; - - if (invert_) - { - // Rotate 180 degrees - cv::flip(img, img, false); - cv::flip(img, img, true); - } - - boost::mutex::scoped_lock lock(send_mutex_); // protects output_size_image - if (output_width_ != input_width || output_height_ != input_height) - { - cv::Mat img_resized; - cv::Size new_size(output_width_, output_height_); - cv::resize(img, img_resized, new_size); - output_size_image = img_resized; - } - else - { - output_size_image = img; - } - - if (!initialized_) - { - initialize(output_size_image); - initialized_ = true; - } - - last_frame = nh_->now(); - sendImage(output_size_image, last_frame ); - - } - catch (cv_bridge::Exception &e) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "cv_bridge exception: %s", e.what()); - inactive_ = true; - return; - } - catch (cv::Exception &e) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "cv_bridge exception: %s", e.what()); - inactive_ = true; - return; - } - catch (boost::system::system_error &e) - { - // happens when client disconnects - RCLCPP_DEBUG(nh_->get_logger(), "system_error exception: %s", e.what()); - inactive_ = true; - return; - } - catch (std::exception &e) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception: %s", e.what()); - inactive_ = true; - return; - } - catch (...) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception"); - inactive_ = true; - return; - } -} - -} diff --git a/src/jpeg_streamers.cpp b/src/jpeg_streamers.cpp deleted file mode 100644 index 0c5cf88c..00000000 --- a/src/jpeg_streamers.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "web_video_server/jpeg_streamers.h" -#include "async_web_server_cpp/http_reply.hpp" - -namespace web_video_server -{ - -MjpegStreamer::MjpegStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - ImageTransportImageStreamer(request, connection, nh), stream_(std::bind(&rclcpp::Node::now, nh), connection) -{ - quality_ = request.get_query_param_value_or_default("quality", 95); - stream_.sendInitialHeader(); -} - -MjpegStreamer::~MjpegStreamer() -{ - this->inactive_ = true; - boost::mutex::scoped_lock lock(send_mutex_); // protects sendImage. -} - -void MjpegStreamer::sendImage(const cv::Mat &img, const rclcpp::Time &time) -{ - std::vector encode_params; - encode_params.push_back(cv::IMWRITE_JPEG_QUALITY); - encode_params.push_back(quality_); - - std::vector encoded_buffer; - cv::imencode(".jpeg", img, encoded_buffer, encode_params); - - stream_.sendPartAndClear(time, "image/jpeg", encoded_buffer); -} - -boost::shared_ptr MjpegStreamerType::create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new MjpegStreamer(request, connection, nh)); -} - -std::string MjpegStreamerType::create_viewer(const async_web_server_cpp::HttpRequest &request) -{ - std::stringstream ss; - ss << ""; - return ss.str(); -} - -JpegSnapshotStreamer::JpegSnapshotStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) : - ImageTransportImageStreamer(request, connection, nh) -{ - quality_ = request.get_query_param_value_or_default("quality", 95); -} - -JpegSnapshotStreamer::~JpegSnapshotStreamer() -{ - this->inactive_ = true; - boost::mutex::scoped_lock lock(send_mutex_); // protects sendImage. -} - -void JpegSnapshotStreamer::sendImage(const cv::Mat &img, const rclcpp::Time &time) -{ - std::vector encode_params; - encode_params.push_back(cv::IMWRITE_JPEG_QUALITY); - encode_params.push_back(quality_); - - std::vector encoded_buffer; - cv::imencode(".jpeg", img, encoded_buffer, encode_params); - - char stamp[20]; - sprintf(stamp, "%.06lf", time.seconds()); - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) - .header("Connection", "close") - .header("Server", "web_video_server") - .header("Cache-Control", - "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, " - "max-age=0") - .header("X-Timestamp", stamp) - .header("Pragma", "no-cache") - .header("Content-type", "image/jpeg") - .header("Access-Control-Allow-Origin", "*") - .header("Content-Length", - boost::lexical_cast(encoded_buffer.size())) - .write(connection_); - connection_->write_and_clear(encoded_buffer); - inactive_ = true; -} - -} diff --git a/src/libav_streamer.cpp b/src/libav_streamer.cpp deleted file mode 100644 index 28d0425f..00000000 --- a/src/libav_streamer.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#include "web_video_server/libav_streamer.h" -#include "async_web_server_cpp/http_reply.hpp" - -/*https://stackoverflow.com/questions/46884682/error-in-building-opencv-with-ffmpeg*/ -#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22) -#define CODEC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER - -namespace web_video_server -{ - -static int ffmpeg_boost_mutex_lock_manager(void **mutex, enum AVLockOp op) -{ - if (NULL == mutex) - return -1; - - switch (op) - { - case AV_LOCK_CREATE: - { - *mutex = NULL; - boost::mutex *m = new boost::mutex(); - *mutex = static_cast(m); - break; - } - case AV_LOCK_OBTAIN: - { - boost::mutex *m = static_cast(*mutex); - m->lock(); - break; - } - case AV_LOCK_RELEASE: - { - boost::mutex *m = static_cast(*mutex); - m->unlock(); - break; - } - case AV_LOCK_DESTROY: - { - boost::mutex *m = static_cast(*mutex); - m->lock(); - m->unlock(); - delete m; - break; - } - default: - break; - } - return 0; -} - -LibavStreamer::LibavStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh, - const std::string &format_name, const std::string &codec_name, - const std::string &content_type) : - ImageTransportImageStreamer(request, connection, nh), output_format_(0), format_context_(0), codec_(0), codec_context_(0), video_stream_( - 0), frame_(0), sws_context_(0), first_image_timestamp_(0), format_name_( - format_name), codec_name_(codec_name), content_type_(content_type), opt_(0), io_buffer_(0) -{ - - bitrate_ = request.get_query_param_value_or_default("bitrate", 100000); - qmin_ = request.get_query_param_value_or_default("qmin", 10); - qmax_ = request.get_query_param_value_or_default("qmax", 42); - gop_ = request.get_query_param_value_or_default("gop", 250); - - av_lockmgr_register(&ffmpeg_boost_mutex_lock_manager); - av_register_all(); -} - -LibavStreamer::~LibavStreamer() -{ - if (codec_context_) - avcodec_close(codec_context_); - if (frame_) - { -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) - av_free(frame_); - frame_ = NULL; -#else - av_frame_free(&frame_); -#endif - } - if (io_buffer_) - delete io_buffer_; - if (format_context_) { - if (format_context_->pb) - av_free(format_context_->pb); - avformat_free_context(format_context_); - } - if (sws_context_) - sws_freeContext(sws_context_); -} - -// output callback for ffmpeg IO context -static int dispatch_output_packet(void* opaque, uint8_t* buffer, int buffer_size) -{ - async_web_server_cpp::HttpConnectionPtr connection = *((async_web_server_cpp::HttpConnectionPtr*) opaque); - std::vector encoded_frame; - encoded_frame.assign(buffer, buffer + buffer_size); - connection->write_and_clear(encoded_frame); - return 0; // TODO: can this fail? -} - -void LibavStreamer::initialize(const cv::Mat &img) -{ - // Load format - format_context_ = avformat_alloc_context(); - if (!format_context_) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error allocating ffmpeg format context"); - } - output_format_ = av_guess_format(format_name_.c_str(), NULL, NULL); - if (!output_format_) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error looking up output format"); - } - format_context_->oformat = output_format_; - - // Set up custom IO callback. - size_t io_buffer_size = 3 * 1024; // 3M seen elsewhere and adjudged good - io_buffer_ = new unsigned char[io_buffer_size]; - AVIOContext* io_ctx = avio_alloc_context(io_buffer_, io_buffer_size, AVIO_FLAG_WRITE, &connection_, NULL, dispatch_output_packet, NULL); - if (!io_ctx) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error setting up IO context"); - } - io_ctx->seekable = 0; // no seeking, it's a stream - format_context_->pb = io_ctx; - output_format_->flags |= AVFMT_FLAG_CUSTOM_IO; - output_format_->flags |= AVFMT_NOFILE; - - // Load codec - if (codec_name_.empty()) // use default codec if none specified - codec_ = avcodec_find_encoder(output_format_->video_codec); - else - codec_ = avcodec_find_encoder_by_name(codec_name_.c_str()); - if (!codec_) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error looking up codec"); - } - video_stream_ = avformat_new_stream(format_context_, codec_); - if (!video_stream_) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error creating video stream"); - } - codec_context_ = video_stream_->codec; - - // Set options - avcodec_get_context_defaults3(codec_context_, codec_); - - codec_context_->codec_id = codec_->id; - codec_context_->bit_rate = bitrate_; - - codec_context_->width = output_width_; - codec_context_->height = output_height_; - codec_context_->delay = 0; - - video_stream_->time_base.num = 1; - video_stream_->time_base.den = 1000; - - codec_context_->time_base.num = 1; - codec_context_->time_base.den = 1; - codec_context_->gop_size = gop_; - codec_context_->pix_fmt = AV_PIX_FMT_YUV420P; - codec_context_->max_b_frames = 0; - - // Quality settings - codec_context_->qmin = qmin_; - codec_context_->qmax = qmax_; - - initializeEncoder(); - - // Some formats want stream headers to be separate - if (format_context_->oformat->flags & AVFMT_GLOBALHEADER) - codec_context_->flags |= CODEC_FLAG_GLOBAL_HEADER; - - // Open Codec - if (avcodec_open2(codec_context_, codec_, NULL) < 0) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Could not open video codec"); - } - - // Allocate frame buffers -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) - frame_ = avcodec_alloc_frame(); -#else - frame_ = av_frame_alloc(); -#endif - av_image_alloc(frame_->data, frame_->linesize, output_width_, output_height_, - codec_context_->pix_fmt, 1); - - frame_->width = output_width_; - frame_->height = output_height_; - frame_->format = codec_context_->pix_fmt; - output_format_->flags |= AVFMT_NOFILE; - - // Generate header - std::vector header_buffer; - std::size_t header_size; - uint8_t *header_raw_buffer; - // define meta data - av_dict_set(&format_context_->metadata, "author", "ROS web_video_server", 0); - av_dict_set(&format_context_->metadata, "title", topic_.c_str(), 0); - - // Send response headers - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok).header("Connection", "close").header( - "Server", "web_video_server").header("Cache-Control", - "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0").header( - "Pragma", "no-cache").header("Expires", "0").header("Max-Age", "0").header("Trailer", "Expires").header( - "Content-type", content_type_).header("Access-Control-Allow-Origin", "*").write(connection_); - - // Send video stream header - if (avformat_write_header(format_context_, &opt_) < 0) - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::internal_server_error)(request_, - connection_, - NULL, NULL); - throw std::runtime_error("Error openning dynamic buffer"); - } -} - -void LibavStreamer::initializeEncoder() -{ -} - -void LibavStreamer::sendImage(const cv::Mat &img, const rclcpp::Time &time) -{ - boost::mutex::scoped_lock lock(encode_mutex_); - if (0 == first_image_timestamp_.nanoseconds()) - { - first_image_timestamp_ = time; - } - std::vector encoded_frame; -#if (LIBAVUTIL_VERSION_MAJOR < 53) - PixelFormat input_coding_format = PIX_FMT_BGR24; -#else - AVPixelFormat input_coding_format = AV_PIX_FMT_BGR24; -#endif - -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) - AVPicture *raw_frame = new AVPicture; - avpicture_fill(raw_frame, img.data, input_coding_format, output_width_, output_height_); -#else - AVFrame *raw_frame = av_frame_alloc(); - av_image_fill_arrays(raw_frame->data, raw_frame->linesize, - img.data, input_coding_format, output_width_, output_height_, 1); -#endif - - - - // Convert from opencv to libav - if (!sws_context_) - { - static int sws_flags = SWS_BICUBIC; - sws_context_ = sws_getContext(output_width_, output_height_, input_coding_format, output_width_, output_height_, - codec_context_->pix_fmt, sws_flags, NULL, NULL, NULL); - if (!sws_context_) - { - throw std::runtime_error("Could not initialize the conversion context"); - } - } - - - int ret = sws_scale(sws_context_, - (const uint8_t * const *)raw_frame->data, raw_frame->linesize, 0, - output_height_, frame_->data, frame_->linesize); - -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) - delete raw_frame; -#else - av_frame_free(&raw_frame); -#endif - - // Encode the frame - AVPacket pkt; - int got_packet; - av_init_packet(&pkt); - -#if (LIBAVCODEC_VERSION_MAJOR < 54) - int buf_size = 6 * output_width_ * output_height_; - pkt.data = (uint8_t*)av_malloc(buf_size); - pkt.size = avcodec_encode_video(codec_context_, pkt.data, buf_size, frame_); - got_packet = pkt.size > 0; -#elif (LIBAVCODEC_VERSION_MAJOR < 57) - pkt.data = NULL; // packet data will be allocated by the encoder - pkt.size = 0; - if (avcodec_encode_video2(codec_context_, &pkt, frame_, &got_packet) < 0) - { - throw std::runtime_error("Error encoding video frame"); - } -#else - pkt.data = NULL; // packet data will be allocated by the encoder - pkt.size = 0; - if (avcodec_send_frame(codec_context_, frame_) < 0) - { - throw std::runtime_error("Error encoding video frame"); - } - if (avcodec_receive_packet(codec_context_, &pkt) < 0) - { - throw std::runtime_error("Error retrieving encoded packet"); - } -#endif - - if (got_packet) - { - std::size_t size; - uint8_t *output_buf; - - double seconds = (time - first_image_timestamp_).seconds(); - // Encode video at 1/0.95 to minimize delay - pkt.pts = (int64_t)(seconds / av_q2d(video_stream_->time_base) * 0.95); - if (pkt.pts <= 0) - pkt.pts = 1; - pkt.dts = AV_NOPTS_VALUE; - - if (pkt.flags&AV_PKT_FLAG_KEY) - pkt.flags |= AV_PKT_FLAG_KEY; - - pkt.stream_index = video_stream_->index; - - if (av_write_frame(format_context_, &pkt)) - { - throw std::runtime_error("Error when writing frame"); - } - } - else - { - encoded_frame.clear(); - } -#if LIBAVCODEC_VERSION_INT < 54 - av_free(pkt.data); -#endif - -#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) - av_free_packet(&pkt); -#else - av_packet_unref(&pkt); -#endif - - connection_->write_and_clear(encoded_frame); -} - -LibavStreamerType::LibavStreamerType(const std::string &format_name, const std::string &codec_name, - const std::string &content_type) : - format_name_(format_name), codec_name_(codec_name), content_type_(content_type) -{ -} - -boost::shared_ptr LibavStreamerType::create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr( - new LibavStreamer(request, connection, nh, format_name_, codec_name_, content_type_)); -} - -std::string LibavStreamerType::create_viewer(const async_web_server_cpp::HttpRequest &request) -{ - std::stringstream ss; - ss << ""; - return ss.str(); -} - -} diff --git a/src/multipart_stream.cpp b/src/multipart_stream.cpp index caf60b70..1aa28df6 100644 --- a/src/multipart_stream.cpp +++ b/src/multipart_stream.cpp @@ -1,84 +1,142 @@ -#include "web_video_server/multipart_stream.h" +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/multipart_stream.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_header.hpp" #include "async_web_server_cpp/http_reply.hpp" namespace web_video_server { MultipartStream::MultipartStream( - std::function get_now, - async_web_server_cpp::HttpConnectionPtr& connection, - const std::string& boundry, - std::size_t max_queue_size) - : get_now_(get_now), connection_(connection), boundry_(boundry), max_queue_size_(max_queue_size) + async_web_server_cpp::HttpConnectionPtr & connection, + const std::string & boundary, + std::size_t max_queue_size) +: max_queue_size_(max_queue_size), connection_(connection), boundary_(boundary) {} -void MultipartStream::sendInitialHeader() { - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok).header("Connection", "close").header( - "Server", "web_video_server").header("Cache-Control", - "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0").header( - "Pragma", "no-cache").header("Content-type", "multipart/x-mixed-replace;boundary="+boundry_).header( - "Access-Control-Allow-Origin", "*").write(connection_); - connection_->write("--"+boundry_+"\r\n"); +void MultipartStream::send_initial_header() +{ + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("Pragma", "no-cache") + .header("Content-type", "multipart/x-mixed-replace;boundary=" + boundary_) + .header("Access-Control-Allow-Origin", "*") + .write(connection_); + connection_->write("--" + boundary_ + "\r\n"); } -void MultipartStream::sendPartHeader(const rclcpp::Time &time, const std::string& type, size_t payload_size) { +void MultipartStream::send_part_header( + const std::chrono::steady_clock::time_point & time, const std::string & type, + size_t payload_size) +{ char stamp[20]; - sprintf(stamp, "%.06lf", time.seconds()); - std::shared_ptr > headers( - new std::vector()); + snprintf( + stamp, sizeof(stamp), "%.06lf", + std::chrono::duration_cast>(time.time_since_epoch()).count()); + const std::shared_ptr> headers( + new std::vector()); headers->push_back(async_web_server_cpp::HttpHeader("Content-type", type)); headers->push_back(async_web_server_cpp::HttpHeader("X-Timestamp", stamp)); headers->push_back( - async_web_server_cpp::HttpHeader("Content-Length", boost::lexical_cast(payload_size))); + async_web_server_cpp::HttpHeader( + "Content-Length", std::to_string(payload_size))); connection_->write(async_web_server_cpp::HttpReply::to_buffers(*headers), headers); } -void MultipartStream::sendPartFooter(const rclcpp::Time &time) { - std::shared_ptr str(new std::string("\r\n--"+boundry_+"\r\n")); +void MultipartStream::send_part_footer(const std::chrono::steady_clock::time_point & time) +{ + const std::shared_ptr str(new std::string("\r\n--" + boundary_ + "\r\n")); PendingFooter pf; pf.timestamp = time; pf.contents = str; connection_->write(boost::asio::buffer(*str), str); - if (max_queue_size_ > 0) pending_footers_.push(pf); + if (max_queue_size_ > 0) {pending_footers_.push(pf);} } -void MultipartStream::sendPartAndClear(const rclcpp::Time &time, const std::string& type, - std::vector &data) { - if (!isBusy()) - { - sendPartHeader(time, type, data.size()); +void MultipartStream::send_part_and_clear( + const std::chrono::steady_clock::time_point & time, const std::string & type, + std::vector & data) +{ + if (!is_busy()) { + send_part_header(time, type, data.size()); connection_->write_and_clear(data); - sendPartFooter(time); + send_part_footer(time); } } -void MultipartStream::sendPart(const rclcpp::Time &time, const std::string& type, - const boost::asio::const_buffer &buffer, - async_web_server_cpp::HttpConnection::ResourcePtr resource) { - if (!isBusy()) - { - sendPartHeader(time, type, boost::asio::buffer_size(buffer)); +void MultipartStream::send_part( + const std::chrono::steady_clock::time_point & time, const std::string & type, + const boost::asio::const_buffer & buffer, + async_web_server_cpp::HttpConnection::ResourcePtr resource) +{ + if (!is_busy()) { + send_part_header(time, type, boost::asio::buffer_size(buffer)); connection_->write(buffer, resource); - sendPartFooter(time); + send_part_footer(time); } } -bool MultipartStream::isBusy() { - rclcpp::Time currentTime = get_now_(); - while (!pending_footers_.empty()) - { +bool MultipartStream::is_busy() +{ + auto current_time = std::chrono::steady_clock::now(); + while (!pending_footers_.empty()) { if (pending_footers_.front().contents.expired()) { pending_footers_.pop(); } else { - rclcpp::Time footerTime = pending_footers_.front().timestamp; - if ((currentTime - footerTime).seconds() > 0.5) { + auto footer_time = pending_footers_.front().timestamp; + if (std::chrono::duration_cast>( + (current_time - footer_time)).count() > 0.5) + { pending_footers_.pop(); } else { break; } } } - return !(max_queue_size_ == 0 || pending_footers_.size() < max_queue_size_); + return max_queue_size_ != 0 && pending_footers_.size() >= max_queue_size_; } -} +} // namespace web_video_server diff --git a/src/png_streamers.cpp b/src/png_streamers.cpp deleted file mode 100644 index 1a9f874e..00000000 --- a/src/png_streamers.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "web_video_server/png_streamers.h" -#include "async_web_server_cpp/http_reply.hpp" - -namespace web_video_server -{ - -PngStreamer::PngStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - ImageTransportImageStreamer(request, connection, nh), stream_(std::bind(&rclcpp::Node::now, nh), connection) -{ - quality_ = request.get_query_param_value_or_default("quality", 3); - stream_.sendInitialHeader(); -} - -PngStreamer::~PngStreamer() -{ - this->inactive_ = true; - boost::mutex::scoped_lock lock(send_mutex_); // protects sendImage. -} - -void PngStreamer::sendImage(const cv::Mat &img, const rclcpp::Time &time) -{ - std::vector encode_params; - encode_params.push_back(cv::IMWRITE_PNG_COMPRESSION); - encode_params.push_back(quality_); - - std::vector encoded_buffer; - cv::imencode(".png", img, encoded_buffer, encode_params); - - stream_.sendPartAndClear(time, "image/png", encoded_buffer); -} - -boost::shared_ptr PngStreamerType::create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new PngStreamer(request, connection, nh)); -} - -std::string PngStreamerType::create_viewer(const async_web_server_cpp::HttpRequest &request) -{ - std::stringstream ss; - ss << ""; - return ss.str(); -} - -PngSnapshotStreamer::PngSnapshotStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) : - ImageTransportImageStreamer(request, connection, nh) -{ - quality_ = request.get_query_param_value_or_default("quality", 3); -} - -PngSnapshotStreamer::~PngSnapshotStreamer() -{ - this->inactive_ = true; - boost::mutex::scoped_lock lock(send_mutex_); // protects sendImage. -} - -void PngSnapshotStreamer::sendImage(const cv::Mat &img, const rclcpp::Time &time) -{ - std::vector encode_params; - encode_params.push_back(cv::IMWRITE_PNG_COMPRESSION); - encode_params.push_back(quality_); - - std::vector encoded_buffer; - cv::imencode(".png", img, encoded_buffer, encode_params); - - char stamp[20]; - sprintf(stamp, "%.06lf", time.seconds()); - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) - .header("Connection", "close") - .header("Server", "web_video_server") - .header("Cache-Control", - "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, " - "max-age=0") - .header("X-Timestamp", stamp) - .header("Pragma", "no-cache") - .header("Content-type", "image/png") - .header("Access-Control-Allow-Origin", "*") - .header("Content-Length", - boost::lexical_cast(encoded_buffer.size())) - .write(connection_); - connection_->write_and_clear(encoded_buffer); - inactive_ = true; -} - -} diff --git a/src/ros_compressed_streamer.cpp b/src/ros_compressed_streamer.cpp deleted file mode 100644 index 0fedaae8..00000000 --- a/src/ros_compressed_streamer.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "web_video_server/ros_compressed_streamer.h" - -namespace web_video_server -{ - -RosCompressedStreamer::RosCompressedStreamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - ImageStreamer(request, connection, nh), stream_(std::bind(&rclcpp::Node::now, nh), connection) -{ - stream_.sendInitialHeader(); -} - -RosCompressedStreamer::~RosCompressedStreamer() -{ - this->inactive_ = true; - boost::mutex::scoped_lock lock(send_mutex_); // protects sendImage. -} - -void RosCompressedStreamer::start() { - std::string compressed_topic = topic_ + "/compressed"; - image_sub_ = nh_->create_subscription( - compressed_topic, 1, std::bind(&RosCompressedStreamer::imageCallback, this, std::placeholders::_1)); -} - -void RosCompressedStreamer::restreamFrame(double max_age) -{ - if (inactive_ || (last_msg == 0)) - return; - - if ( last_frame + rclcpp::Duration::from_seconds(max_age) < nh_->now() ) { - boost::mutex::scoped_lock lock(send_mutex_); - sendImage(last_msg, nh_->now() ); // don't update last_frame, it may remain an old value. - } -} - -void RosCompressedStreamer::sendImage(const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, - const rclcpp::Time &time) { - try { - std::string content_type; - if(msg->format.find("jpeg") != std::string::npos) { - content_type = "image/jpeg"; - } - else if(msg->format.find("png") != std::string::npos) { - content_type = "image/png"; - } - else { - RCLCPP_WARN(nh_->get_logger(), "Unknown ROS compressed image format: %s", msg->format.c_str()); - return; - } - - stream_.sendPart(time, content_type, boost::asio::buffer(msg->data), msg); - } - catch (boost::system::system_error &e) - { - // happens when client disconnects - RCLCPP_DEBUG(nh_->get_logger(), "system_error exception: %s", e.what()); - inactive_ = true; - return; - } - catch (std::exception &e) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception: %s", e.what()); - inactive_ = true; - return; - } - catch (...) - { - // TODO THROTTLE with 30 - RCLCPP_ERROR(nh_->get_logger(), "exception"); - inactive_ = true; - return; - } -} - - -void RosCompressedStreamer::imageCallback(const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg) { - boost::mutex::scoped_lock lock(send_mutex_); // protects last_msg and last_frame - last_msg = msg; - last_frame = rclcpp::Time(msg->header.stamp); - sendImage(last_msg, last_frame); -} - - -boost::shared_ptr RosCompressedStreamerType::create_streamer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new RosCompressedStreamer(request, connection, nh)); -} - -std::string RosCompressedStreamerType::create_viewer(const async_web_server_cpp::HttpRequest &request) -{ - std::stringstream ss; - ss << ""; - return ss.str(); -} - - -} diff --git a/src/streamer.cpp b/src/streamer.cpp new file mode 100644 index 00000000..63411d5f --- /dev/null +++ b/src/streamer.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamer.hpp" + +#include +#include +#include +#include + +#include "rclcpp/node.hpp" +#include "rclcpp/logging.hpp" + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" + +namespace web_video_server +{ + +StreamerBase::StreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node, + std::string logger_name) +: connection_(connection), request_(request), node_(std::move(node)), + logger_(node_.lock()->get_logger().get_child(logger_name)), inactive_(false), + topic_(request.get_query_param_value_or_default("topic", "")), + client_id_(request.get_query_param_value_or_default("client_id", "")) +{ +} + +rclcpp::Node::SharedPtr StreamerBase::lock_node() const +{ + auto node = node_.lock(); + if (!node) { + RCLCPP_WARN(logger_, "Unable to access node because the owning node has been destroyed"); + } + return node; +} + +std::string StreamerFactoryInterface::create_viewer( + const async_web_server_cpp::HttpRequest & request) +{ + std::stringstream ss; + ss << ""; + return ss.str(); +} + +std::vector StreamerFactoryInterface::get_available_topics( + rclcpp::Node & /* node */) +{ + return {}; +} + +} // namespace web_video_server diff --git a/src/streamers/h264_streamer.cpp b/src/streamers/h264_streamer.cpp new file mode 100644 index 00000000..a96d872b --- /dev/null +++ b/src/streamers/h264_streamer.cpp @@ -0,0 +1,102 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/h264_streamer.hpp" + +extern "C" +{ +#include +#include +#include +#include +} + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +H264Streamer::H264Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: LibavStreamerBase(request, connection, node, "h264_streamer", "mp4", "libx264", "video/mp4") +{ + /* possible quality presets: + * ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo + * no latency improvements observed with ultrafast instead of medium + */ + preset_ = request.get_query_param_value_or_default("preset", "ultrafast"); +} + +H264Streamer::~H264Streamer() +{ +} + +void H264Streamer::initialize_encoder() +{ + av_opt_set(codec_context_->priv_data, "preset", preset_.c_str(), 0); + av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0); + av_opt_set_int(codec_context_->priv_data, "crf", 20, 0); + av_opt_set_int(codec_context_->priv_data, "bufsize", 100, 0); + av_opt_set_int(codec_context_->priv_data, "keyint", 30, 0); + av_opt_set_int(codec_context_->priv_data, "g", 1, 0); + + // container format options + if (strcmp(format_context_->oformat->name, "mp4") == 0) { + // set up mp4 for streaming (instead of seekable file output) + av_dict_set(&opt_, "movflags", "+frag_keyframe+empty_moov+faststart", 0); + } +} + +std::shared_ptr H264StreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::H264StreamerFactory, + web_video_server::StreamerFactoryInterface) diff --git a/src/streamers/image_transport_streamer.cpp b/src/streamers/image_transport_streamer.cpp new file mode 100644 index 00000000..4d84b09e --- /dev/null +++ b/src/streamers/image_transport_streamer.cpp @@ -0,0 +1,362 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/image_transport_streamer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef CV_BRIDGE_USES_OLD_HEADERS +#include "cv_bridge/cv_bridge.h" +#else +#include "cv_bridge/cv_bridge.hpp" +#endif + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "image_transport/image_transport.hpp" +#include "image_transport/transport_hints.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/qos.hpp" +#include "rmw/qos_profiles.h" +#include "sensor_msgs/msg/image.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/utils.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +namespace +{ + +std::vector get_image_topics(rclcpp::Node & node) +{ + std::vector result; + auto topic_names_and_types = node.get_topic_names_and_types(); + for (const auto & topic_and_types : topic_names_and_types) { + for (const auto & type : topic_and_types.second) { + if (type == "sensor_msgs/msg/Image") { + result.push_back(topic_and_types.first); + break; + } + } + } + return result; +} + +std::optional detect_publisher_qos( + const rclcpp::Node::SharedPtr & node, + const rclcpp::Logger & logger, + const std::string & topic) +{ + RCLCPP_INFO(logger, "Attempting to auto-detect QoS for topic: %s", topic.c_str()); + + const auto topic_endpoint_info_array = node->get_publishers_info_by_topic(topic); + if (topic_endpoint_info_array.empty()) { + RCLCPP_WARN(logger, "No publishers found for topic: %s", topic.c_str()); + return std::nullopt; + } + + // Use the first publisher's QoS as reference. + const auto & endpoint_info = topic_endpoint_info_array.front(); + const auto qos_profile = endpoint_info.qos_profile(); + + const std::string reliability = + (qos_profile.reliability() == rclcpp::ReliabilityPolicy::Reliable) ? "RELIABLE" : "BEST_EFFORT"; + const std::string durability = + (qos_profile.durability() == rclcpp::DurabilityPolicy::TransientLocal) ? + "TRANSIENT_LOCAL" : "VOLATILE"; + + RCLCPP_INFO( + logger, "Detected QoS - Reliability: %s, Durability: %s, History depth: %zu", + reliability.c_str(), durability.c_str(), qos_profile.depth()); + + // Convert rclcpp QoS to rmw QoS profile. + auto rmw_qos = rmw_qos_profile_default; + rmw_qos.reliability = RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT; + rmw_qos.durability = RMW_QOS_POLICY_DURABILITY_VOLATILE; + rmw_qos.history = RMW_QOS_POLICY_HISTORY_KEEP_LAST; + rmw_qos.depth = qos_profile.depth(); + + if (qos_profile.reliability() == rclcpp::ReliabilityPolicy::Reliable) { + rmw_qos.reliability = RMW_QOS_POLICY_RELIABILITY_RELIABLE; + } + if (qos_profile.durability() == rclcpp::DurabilityPolicy::TransientLocal) { + rmw_qos.durability = RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL; + } + + return rmw_qos; +} + +std::optional get_qos_profile( + const rclcpp::Node::SharedPtr & node, + const rclcpp::Logger & logger, + const std::string & profile_name, + const std::string & topic) +{ + if (profile_name == "auto") { + auto qos_profile = detect_publisher_qos(node, logger, topic); + if (!qos_profile) { + RCLCPP_WARN( + logger, "Could not auto-detect QoS for topic %s. Using default profile.", topic.c_str()); + return rmw_qos_profile_default; + } + RCLCPP_INFO(logger, "Using auto-detected QoS profile for topic %s", topic.c_str()); + return qos_profile; + } + + RCLCPP_INFO( + logger, "Using specified QoS profile %s for topic %s", profile_name.c_str(), topic.c_str()); + auto qos_profile = web_video_server::get_qos_profile_from_name(profile_name); + if (!qos_profile) { + RCLCPP_ERROR( + logger, "Invalid QoS profile %s specified. Using default profile.", profile_name.c_str()); + return rmw_qos_profile_default; + } + return qos_profile; +} + +} // namespace + +ImageTransportStreamerBase::ImageTransportStreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node, + std::string logger_name) +: StreamerBase(request, connection, node, logger_name), initialized_(false) +{ + output_width_ = request.get_query_param_value_or_default("width", -1); + output_height_ = request.get_query_param_value_or_default("height", -1); + invert_ = request.has_query_param("invert"); + default_transport_ = request.get_query_param_value_or_default("default_transport", "raw"); + qos_profile_name_ = request.get_query_param_value_or_default("qos_profile", "auto"); +} + +ImageTransportStreamerBase::~ImageTransportStreamerBase() +{ +} + +// We disable deprecation warnings for image_transport API usage +// to maintain compatibility with older ROS 2 distributions. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +// NOLINTBEGIN(clang-diagnostic-deprecated-declarations) + +void ImageTransportStreamerBase::start() +{ + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + const image_transport::TransportHints hints(node.get(), default_transport_); + auto tnat = node->get_topic_names_and_types(); + inactive_ = true; + for (auto topic_and_types : tnat) { + if (topic_and_types.second.size() > 1) { + // skip over topics with more than one type + continue; + } + const auto & topic_name = topic_and_types.first; + if (topic_name == topic_ || (topic_name.find("/") == 0 && topic_name.substr(1) == topic_)) { + inactive_ = false; + break; + } + } + + // Get QoS profile based on user selection or auto-detect. + const auto qos_profile = get_qos_profile(node, logger_, qos_profile_name_, topic_); + + // Create subscriber + image_sub_ = image_transport::create_subscription( + node.get(), topic_, + std::bind(&ImageTransportStreamerBase::image_callback, this, std::placeholders::_1), + default_transport_, qos_profile.value()); +} + +#pragma GCC diagnostic pop +// NOLINTEND(clang-diagnostic-deprecated-declarations) + +void ImageTransportStreamerBase::initialize(const cv::Mat & /*img*/) +{ +} + +void ImageTransportStreamerBase::restream_frame(std::chrono::duration/* max_age */) +{ + if (inactive_ || !initialized_) { + return; + } + + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + try_send_image(output_size_image_, last_frame_, *node); +} + +void ImageTransportStreamerBase::image_callback(const sensor_msgs::msg::Image::ConstSharedPtr & msg) +{ + if (inactive_) { + return; + } + + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + cv::Mat img; + try { + img = decode_image(msg); + const int input_width = img.cols; + const int input_height = img.rows; + + if (output_width_ == -1) { + output_width_ = input_width; + } + if (output_height_ == -1) { + output_height_ = input_height; + } + + if (invert_) { + // Rotate 180 degrees + cv::flip(img, img, 0); + cv::flip(img, img, 1); + } + + const std::scoped_lock lock(send_mutex_); // protects output_size_image_ + if (output_width_ != input_width || output_height_ != input_height) { + cv::Mat img_resized; + const cv::Size new_size(output_width_, output_height_); + cv::resize(img, img_resized, new_size); + output_size_image_ = img_resized; + } else { + output_size_image_ = img; + } + + if (!initialized_) { + initialize(output_size_image_); + initialized_ = true; + } + + last_frame_ = std::chrono::steady_clock::now(); + } catch (cv_bridge::Exception & e) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "cv_bridge exception: %s", e.what()); + inactive_ = true; + return; + } catch (cv::Exception & e) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "OpenCV exception: %s", e.what()); + inactive_ = true; + return; + } + + try_send_image(output_size_image_, last_frame_, *node); +} + +void ImageTransportStreamerBase::try_send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & /* time */, + rclcpp::Node & node) +{ + try { + const std::scoped_lock lock(send_mutex_); + send_image(img, std::chrono::steady_clock::now()); + } catch (boost::system::system_error & e) { + // happens when client disconnects + RCLCPP_DEBUG(logger_, "system_error exception: %s", e.what()); + inactive_ = true; + return; + } catch (std::exception & e) { + auto & clk = *node.get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception: %s", e.what()); + inactive_ = true; + return; + } catch (...) { + auto & clk = *node.get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception"); + inactive_ = true; + return; + } +} + +cv::Mat ImageTransportStreamerBase::decode_image( + const sensor_msgs::msg::Image::ConstSharedPtr & msg) +{ + if (msg->encoding.find("F") != std::string::npos) { + // scale floating point images + const cv::Mat float_image_bridge = cv_bridge::toCvCopy(msg, msg->encoding)->image; + cv::Mat_ float_image = float_image_bridge; + double max_val; + cv::minMaxIdx(float_image, 0, &max_val); + + if (max_val > 0) { + float_image *= (255 / max_val); + } + return float_image; + } + // Convert to OpenCV native BGR color + return cv_bridge::toCvCopy(msg, "bgr8")->image; +} + +std::vector ImageTransportStreamerFactoryBase::get_available_topics( + rclcpp::Node & node) +{ + return get_image_topics(node); +} + +std::vector ImageTransportSnapshotStreamerFactoryBase::get_available_topics( + rclcpp::Node & node) +{ + return get_image_topics(node); +} + +} // namespace streamers +} // namespace web_video_server diff --git a/src/streamers/jpeg_streamers.cpp b/src/streamers/jpeg_streamers.cpp new file mode 100644 index 00000000..c0abf0f7 --- /dev/null +++ b/src/streamers/jpeg_streamers.cpp @@ -0,0 +1,161 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/jpeg_streamers.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_reply.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/image_transport_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +MjpegStreamer::MjpegStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: ImageTransportStreamerBase(request, connection, node, "mjpeg_streamer"), + stream_(connection) +{ + quality_ = request.get_query_param_value_or_default("quality", 95); + stream_.send_initial_header(); +} + +MjpegStreamer::~MjpegStreamer() +{ + this->inactive_ = true; + const std::scoped_lock lock(send_mutex_); // protects send_image. +} + +void MjpegStreamer::send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) +{ + std::vector encode_params; + encode_params.push_back(cv::IMWRITE_JPEG_QUALITY); + encode_params.push_back(quality_); + + std::vector encoded_buffer; + cv::imencode(".jpeg", img, encoded_buffer, encode_params); + + stream_.send_part_and_clear(time, "image/jpeg", encoded_buffer); +} + +std::shared_ptr MjpegStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +JpegSnapshotStreamer::JpegSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +: ImageTransportStreamerBase(request, connection, node, "jpeg_snapshot_streamer") +{ + quality_ = request.get_query_param_value_or_default("quality", 95); +} + +JpegSnapshotStreamer::~JpegSnapshotStreamer() +{ + this->inactive_ = true; + const std::scoped_lock lock(send_mutex_); // protects send_image. +} + +void JpegSnapshotStreamer::send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) +{ + std::vector encode_params; + encode_params.push_back(cv::IMWRITE_JPEG_QUALITY); + encode_params.push_back(quality_); + + std::vector encoded_buffer; + cv::imencode(".jpeg", img, encoded_buffer, encode_params); + + char stamp[20]; + snprintf( + stamp, sizeof(stamp), "%.06lf", + std::chrono::duration_cast>(time.time_since_epoch()).count()); + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("X-Timestamp", stamp) + .header("Pragma", "no-cache") + .header("Content-type", "image/jpeg") + .header("Access-Control-Allow-Origin", "*") + .header("Content-Length", std::to_string(encoded_buffer.size())) + .write(connection_); + connection_->write_and_clear(encoded_buffer); + inactive_ = true; +} + +std::shared_ptr JpegSnapshotStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, std::move(node)); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::MjpegStreamerFactory, + web_video_server::StreamerFactoryInterface) +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::JpegSnapshotStreamerFactory, + web_video_server::SnapshotStreamerFactoryInterface) diff --git a/src/streamers/libav_streamer.cpp b/src/streamers/libav_streamer.cpp new file mode 100644 index 00000000..06e0a451 --- /dev/null +++ b/src/streamers/libav_streamer.cpp @@ -0,0 +1,350 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/libav_streamer.hpp" + +extern "C" +{ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_reply.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/logging.hpp" + +#include "web_video_server/streamers/image_transport_streamer.hpp" + +// https://stackoverflow.com/questions/46884682/error-in-building-opencv-with-ffmpeg +#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22) +#define CODEC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER + +namespace web_video_server +{ +namespace streamers +{ + +LibavStreamerBase::LibavStreamerBase( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node, + std::string logger_name, const std::string & format_name, const std::string & codec_name, + const std::string & content_type) +: ImageTransportStreamerBase(request, connection, node, logger_name), format_context_(0), codec_(0), + codec_context_(0), video_stream_(0), opt_(0), frame_(0), sws_context_(0), + first_image_received_(false), format_name_(format_name), codec_name_(codec_name), + content_type_(content_type), io_buffer_(0) +{ + bitrate_ = request.get_query_param_value_or_default("bitrate", 100000); + qmin_ = request.get_query_param_value_or_default("qmin", 10); + qmax_ = request.get_query_param_value_or_default("qmax", 42); + gop_ = request.get_query_param_value_or_default("gop", 25); +} + +LibavStreamerBase::~LibavStreamerBase() +{ + if (codec_context_ != nullptr) { + avcodec_free_context(&codec_context_); + } + if (frame_ != nullptr) { + av_frame_free(&frame_); + } + delete io_buffer_; + if (format_context_ != nullptr) { + if (format_context_->pb != nullptr) { + av_free(format_context_->pb); + } + avformat_free_context(format_context_); + } + if (sws_context_ != nullptr) { + sws_freeContext(sws_context_); + } +} + +namespace +{ +// output callback for ffmpeg IO context +#if LIBAVFORMAT_VERSION_MAJOR < 61 // NOLINT(misc-include-cleaner) +int dispatch_output_packet(void * opaque, uint8_t * buffer, int buffer_size) +#else +int dispatch_output_packet(void * opaque, const uint8_t * buffer, int buffer_size) +#endif +{ + const async_web_server_cpp::HttpConnectionPtr connection = + *(static_cast(opaque)); + std::vector encoded_frame; + encoded_frame.assign(buffer, buffer + buffer_size); + connection->write_and_clear(encoded_frame); + return 0; +} +} // namespace + +void LibavStreamerBase::initialize(const cv::Mat & /* img */) +{ + // Load format + format_context_ = avformat_alloc_context(); + if (format_context_ == nullptr) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error allocating ffmpeg format context"); + } + + format_context_->oformat = av_guess_format(format_name_.c_str(), NULL, NULL); + if (format_context_->oformat == nullptr) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error looking up output format"); + } + + // Set up custom IO callback. + const size_t io_buffer_size = 3 * 1024; // 3M seen elsewhere and adjudged good + io_buffer_ = new unsigned char[io_buffer_size]; + AVIOContext * io_ctx = avio_alloc_context( + io_buffer_, io_buffer_size, AVIO_FLAG_WRITE, + &connection_, NULL, dispatch_output_packet, NULL); + if (io_ctx == nullptr) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error setting up IO context"); + } + io_ctx->seekable = 0; // no seeking, it's a stream + format_context_->pb = io_ctx; + format_context_->max_interleave_delta = 0; + + // Load codec + if (codec_name_.empty()) { // use default codec if none specified + codec_ = avcodec_find_encoder(format_context_->oformat->video_codec); + } else { + codec_ = avcodec_find_encoder_by_name(codec_name_.c_str()); + } + if (codec_ == nullptr) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error looking up codec"); + } + video_stream_ = avformat_new_stream(format_context_, codec_); + if (video_stream_ == nullptr) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error creating video stream"); + } + + codec_context_ = avcodec_alloc_context3(codec_); + + // Set options + codec_context_->codec_id = codec_->id; + codec_context_->bit_rate = bitrate_; + + codec_context_->width = output_width_; + codec_context_->height = output_height_; + codec_context_->delay = 0; + + video_stream_->time_base.num = 1; + video_stream_->time_base.den = 1000; + + codec_context_->time_base.num = 1; + codec_context_->time_base.den = 1; + codec_context_->gop_size = gop_; + codec_context_->pix_fmt = AV_PIX_FMT_YUV420P; + codec_context_->max_b_frames = 0; + + // Quality settings + codec_context_->qmin = qmin_; + codec_context_->qmax = qmax_; + + codec_context_->flags |= AV_CODEC_FLAG_LOW_DELAY; + + initialize_encoder(); + + avcodec_parameters_from_context(video_stream_->codecpar, codec_context_); + + // Open Codec + if (avcodec_open2(codec_context_, codec_, NULL) < 0) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Could not open video codec"); + } + + // Allocate frame buffers + frame_ = av_frame_alloc(); + + av_image_alloc( + frame_->data, frame_->linesize, output_width_, output_height_, + codec_context_->pix_fmt, 1); + + frame_->width = output_width_; + frame_->height = output_height_; + frame_->format = codec_context_->pix_fmt; + + // define meta data + av_dict_set(&format_context_->metadata, "author", "ROS web_video_server", 0); + av_dict_set(&format_context_->metadata, "title", topic_.c_str(), 0); + + // Send response headers + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("Pragma", "no-cache") + .header("Expires", "0") + .header("Max-Age", "0") + .header("Trailer", "Expires") + .header("Content-type", content_type_) + .header("Access-Control-Allow-Origin", "*") + .write(connection_); + + // Send video stream header + if (avformat_write_header(format_context_, &opt_) < 0) { + async_web_server_cpp::HttpReply::stock_reply( + async_web_server_cpp::HttpReply::internal_server_error)(request_, connection_, NULL, NULL); + throw std::runtime_error("Error openning dynamic buffer"); + } +} + +void LibavStreamerBase::send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) +{ + const std::scoped_lock lock(encode_mutex_); + if (!first_image_received_) { + first_image_received_ = true; + first_image_time_ = time; + } + + const AVPixelFormat input_coding_format = AV_PIX_FMT_BGR24; + + AVFrame * raw_frame = av_frame_alloc(); + av_image_fill_arrays( + raw_frame->data, raw_frame->linesize, + img.data, input_coding_format, output_width_, output_height_, 1); + + // Convert from opencv to libav + if (sws_context_ == nullptr) { + static const int sws_flags = SWS_BICUBIC; + sws_context_ = sws_getContext( + output_width_, output_height_, input_coding_format, output_width_, + output_height_, codec_context_->pix_fmt, sws_flags, NULL, NULL, NULL); + if (sws_context_ == nullptr) { + throw std::runtime_error("Could not initialize the conversion context"); + } + } + + + sws_scale( + sws_context_, + static_cast(raw_frame->data), raw_frame->linesize, 0, + output_height_, frame_->data, frame_->linesize); + + av_frame_free(&raw_frame); + + // Encode the frame + AVPacket * pkt = av_packet_alloc(); + + int ret = avcodec_send_frame(codec_context_, frame_); + if (ret == AVERROR_EOF) { + RCLCPP_DEBUG_STREAM(logger_, "avcodec_send_frame() encoder flushed\n"); + } else if (ret == AVERROR(EAGAIN)) { + RCLCPP_DEBUG_STREAM(logger_, "avcodec_send_frame() need output read out\n"); + } + if (ret < 0) { + throw std::runtime_error("Error encoding video frame"); + } + + ret = avcodec_receive_packet(codec_context_, pkt); + bool got_packet = pkt->size > 0; + if (ret == AVERROR_EOF) { + RCLCPP_DEBUG_STREAM(logger_, "avcodec_receive_packet() encoder flushed\n"); + } else if (ret == AVERROR(EAGAIN)) { + RCLCPP_DEBUG_STREAM(logger_, "avcodec_receive_packet() needs more input\n"); + got_packet = false; + } + + if (got_packet) { + const double seconds = std::chrono::duration_cast>( + time - first_image_time_).count(); + // Encode video at 1/0.95 to minimize delay + pkt->pts = static_cast(seconds / av_q2d(video_stream_->time_base) * 0.95); + if (pkt->pts <= 0) { + pkt->pts = 1; + } + pkt->dts = pkt->pts; + + if ((pkt->flags & AV_PKT_FLAG_KEY) != 0) { + pkt->flags |= AV_PKT_FLAG_KEY; + } + + pkt->stream_index = video_stream_->index; + + if (av_write_frame(format_context_, pkt) < 0) { + throw std::runtime_error("Error when writing frame"); + } + } + + av_packet_unref(pkt); +} + +std::string LibavStreamerFactoryBase::create_viewer( + const async_web_server_cpp::HttpRequest & request) +{ + std::stringstream ss; + ss << ""; + return ss.str(); +} + +} // namespace streamers +} // namespace web_video_server diff --git a/src/streamers/png_streamers.cpp b/src/streamers/png_streamers.cpp new file mode 100644 index 00000000..3a0b8324 --- /dev/null +++ b/src/streamers/png_streamers.cpp @@ -0,0 +1,186 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/png_streamers.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_reply.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" +#include "sensor_msgs/image_encodings.hpp" +#include "sensor_msgs/msg/image.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/image_transport_streamer.hpp" + +#ifdef CV_BRIDGE_USES_OLD_HEADERS +#include "cv_bridge/cv_bridge.h" +#else +#include "cv_bridge/cv_bridge.hpp" +#endif + +namespace web_video_server +{ +namespace streamers +{ + +PngStreamer::PngStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: ImageTransportStreamerBase(request, connection, node, "png_streamer"), stream_(connection) +{ + quality_ = request.get_query_param_value_or_default("quality", 3); + stream_.send_initial_header(); +} + +PngStreamer::~PngStreamer() +{ + this->inactive_ = true; + const std::scoped_lock lock(send_mutex_); // protects send_image. +} + +cv::Mat PngStreamer::decode_image(const sensor_msgs::msg::Image::ConstSharedPtr & msg) +{ + // Handle alpha values since PNG supports it + if (sensor_msgs::image_encodings::hasAlpha(msg->encoding)) { + return cv_bridge::toCvCopy(msg, "bgra8")->image; + } + // Use the normal decode otherwise + return ImageTransportStreamerBase::decode_image(msg); +} + +void PngStreamer::send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) +{ + std::vector encode_params; + encode_params.push_back(cv::IMWRITE_PNG_COMPRESSION); + encode_params.push_back(quality_); + + std::vector encoded_buffer; + cv::imencode(".png", img, encoded_buffer, encode_params); + + stream_.send_part_and_clear(time, "image/png", encoded_buffer); +} + +std::shared_ptr PngStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +PngSnapshotStreamer::PngSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +: ImageTransportStreamerBase(request, connection, node) +{ + quality_ = request.get_query_param_value_or_default("quality", 3); +} + +PngSnapshotStreamer::~PngSnapshotStreamer() +{ + this->inactive_ = true; + const std::scoped_lock lock(send_mutex_); // protects send_image. +} + +cv::Mat PngSnapshotStreamer::decode_image(const sensor_msgs::msg::Image::ConstSharedPtr & msg) +{ + // Handle alpha values since PNG supports it + if (sensor_msgs::image_encodings::hasAlpha(msg->encoding)) { + return cv_bridge::toCvCopy(msg, "bgra8")->image; + } + // Use the normal decode otherwise + return ImageTransportStreamerBase::decode_image(msg); +} + +void PngSnapshotStreamer::send_image( + const cv::Mat & img, + const std::chrono::steady_clock::time_point & time) +{ + std::vector encode_params; + encode_params.push_back(cv::IMWRITE_PNG_COMPRESSION); + encode_params.push_back(quality_); + + std::vector encoded_buffer; + cv::imencode(".png", img, encoded_buffer, encode_params); + + char stamp[20]; + snprintf( + stamp, sizeof(stamp), "%.06lf", + std::chrono::duration_cast>(time.time_since_epoch()).count()); + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("X-Timestamp", stamp) + .header("Pragma", "no-cache") + .header("Content-type", "image/png") + .header("Access-Control-Allow-Origin", "*") + .header("Content-Length", std::to_string(encoded_buffer.size())) + .write(connection_); + connection_->write_and_clear(encoded_buffer); + inactive_ = true; +} + +std::shared_ptr PngSnapshotStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::PngStreamerFactory, + web_video_server::StreamerFactoryInterface) +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::PngSnapshotStreamerFactory, + web_video_server::SnapshotStreamerFactoryInterface) diff --git a/src/streamers/ros_compressed_streamer.cpp b/src/streamers/ros_compressed_streamer.cpp new file mode 100644 index 00000000..2e0ca141 --- /dev/null +++ b/src/streamers/ros_compressed_streamer.cpp @@ -0,0 +1,415 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/ros_compressed_streamer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_reply.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/logger.hpp" +#include "rclcpp/logging.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/qos.hpp" +#include "rclcpp/subscription.hpp" +#include "rmw/qos_profiles.h" +#include "sensor_msgs/msg/compressed_image.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/jpeg_streamers.hpp" +#include "web_video_server/utils.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +namespace +{ + +using sensor_msgs::msg::CompressedImage; + +rclcpp::QoS make_compressed_qos( + const std::string & compressed_topic, + const std::string & qos_profile_name, + const rclcpp::Logger & logger) +{ + RCLCPP_INFO( + logger, "Streaming topic %s with QoS profile %s", + compressed_topic.c_str(), qos_profile_name.c_str()); + auto qos_profile = get_qos_profile_from_name(qos_profile_name); + if (!qos_profile) { + RCLCPP_ERROR( + logger, + "Invalid QoS profile %s specified. Using default profile.", + qos_profile_name.c_str()); + qos_profile = rmw_qos_profile_default; + } + + return rclcpp::QoS( + rclcpp::QoSInitialization(qos_profile.value().history, 1), + qos_profile.value()); +} + +std::optional resolve_content_type( + const std::string & format, const rclcpp::Logger & logger) +{ + if (format.find("jpeg") != std::string::npos || format.find("jpg") != std::string::npos) { + return std::string("image/jpeg"); + } + if (format.find("png") != std::string::npos) { + return std::string("image/png"); + } + + RCLCPP_WARN( + logger, "Unknown ROS compressed image format: %s", + format.c_str()); + return std::nullopt; +} + +template +rclcpp::Subscription::SharedPtr create_compressed_image_subscription( + const std::string & topic, + const std::string & qos_profile_name, + const rclcpp::Node::SharedPtr & node, + const rclcpp::Logger & logger, + CallbackT && callback) +{ + const std::string compressed_topic = topic + "/compressed"; + const auto qos = make_compressed_qos(compressed_topic, qos_profile_name, logger); + return node->create_subscription( + compressed_topic, qos, std::forward(callback)); +} + +bool has_compressed_topic(rclcpp::Node & node, const std::string & topic) +{ + const auto compressed_topic_name = topic + "/compressed"; + const auto tnat = node.get_topic_names_and_types(); + return std::any_of( + tnat.begin(), tnat.end(), [&](const auto & topic_and_types) { + if (topic_and_types.second.size() > 1) { + return false; + } + const auto & topic_name = topic_and_types.first; + /* *INDENT-OFF* */ + return topic_name == compressed_topic_name || + (topic_name.rfind('/') == 0 && topic_name.substr(1) == compressed_topic_name); + /* *INDENT-ON* */ + }); +} + +std::vector collect_compressed_topics(rclcpp::Node & node) +{ + std::vector result; + const auto tnat = node.get_topic_names_and_types(); + for (const auto & topic_and_types : tnat) { + for (const auto & type : topic_and_types.second) { + if (type == "sensor_msgs/msg/CompressedImage") { + std::string topic_name = topic_and_types.first; + if (topic_name.size() > 11 && + topic_name.substr(topic_name.size() - 11) == "/compressed") + { + topic_name = topic_name.substr(0, topic_name.size() - 11); + result.push_back(topic_name); + } + } + } + } + return result; +} + +} // namespace + +RosCompressedStreamer::RosCompressedStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +: StreamerBase(request, connection, node, "ros_compressed_streamer"), stream_(connection) +{ + stream_.send_initial_header(); + qos_profile_name_ = request.get_query_param_value_or_default("qos_profile", "default"); +} + +RosCompressedStreamer::~RosCompressedStreamer() +{ + this->inactive_ = true; + const std::scoped_lock lock(send_mutex_); // protects send_image. +} + +void RosCompressedStreamer::start() +{ + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + image_sub_ = create_compressed_image_subscription( + topic_, qos_profile_name_, node, logger_, + std::bind(&RosCompressedStreamer::image_callback, this, std::placeholders::_1)); +} + +void RosCompressedStreamer::restream_frame(std::chrono::duration max_age) +{ + if (inactive_ || (last_msg_ == 0)) { + return; + } + + if (last_frame_ + max_age < std::chrono::steady_clock::now()) { + const std::scoped_lock lock(send_mutex_); + // don't update last_frame, it may remain an old value. + send_image(last_msg_, std::chrono::steady_clock::now()); + } +} + +void RosCompressedStreamer::send_image( + const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, + const std::chrono::steady_clock::time_point & time) +{ + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + auto content_type = resolve_content_type(msg->format, logger_); + if (!content_type) { + return; + } + + try { + stream_.send_part(time, *content_type, boost::asio::buffer(msg->data), msg); + } catch (boost::system::system_error & e) { + // happens when client disconnects + RCLCPP_DEBUG(logger_, "system_error exception: %s", e.what()); + inactive_ = true; + return; + } catch (std::exception & e) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception: %s", e.what()); + inactive_ = true; + return; + } catch (...) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception"); + inactive_ = true; + return; + } +} + + +void RosCompressedStreamer::image_callback( + const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg) +{ + const std::scoped_lock lock(send_mutex_); // protects last_msg_ and last_frame_ + last_msg_ = msg; + last_frame_ = std::chrono::steady_clock::now(); + send_image(last_msg_, last_frame_); +} + + +std::shared_ptr RosCompressedStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + auto node_locked = node.lock(); + if (!node_locked) { + RCLCPP_WARN( + rclcpp::get_logger("web_video_server.RosCompressedStreamerFactory"), + "Cannot create ROS compressed streamer because the node has expired"); + return nullptr; + } + + const std::string topic = request.get_query_param_value_or_default("topic", ""); + if (!has_compressed_topic(*node_locked, topic)) { + RCLCPP_WARN( + node_locked->get_logger().get_child("RosCompressedStreamerFactory"), + "Could not find compressed image topic for %s, falling back to mjpeg", topic.c_str()); + return std::make_shared(request, connection, node); + } + + return std::make_shared(request, connection, node); +} + +std::vector RosCompressedStreamerFactory::get_available_topics( + rclcpp::Node & node) +{ + return collect_compressed_topics(node); +} + +RosCompressedSnapshotStreamer::RosCompressedSnapshotStreamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: StreamerBase(request, connection, node, "ros_compressed_snapshot_streamer") +{ + qos_profile_name_ = request.get_query_param_value_or_default("qos_profile", "default"); +} + +RosCompressedSnapshotStreamer::~RosCompressedSnapshotStreamer() +{ + this->inactive_ = true; +} + +void RosCompressedSnapshotStreamer::start() +{ + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + image_sub_ = create_compressed_image_subscription( + topic_, qos_profile_name_, node, logger_, + std::bind(&RosCompressedSnapshotStreamer::image_callback, this, std::placeholders::_1)); +} + +void RosCompressedSnapshotStreamer::restream_frame(std::chrono::duration/* max_age */) +{ + // no-op, snapshot streamer doesn't restream frames +} + +void RosCompressedSnapshotStreamer::send_image( + const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg, + const std::chrono::steady_clock::time_point & time) +{ + auto node = lock_node(); + if (!node) { + inactive_ = true; + return; + } + + auto content_type = resolve_content_type(msg->format, node->get_logger()); + if (!content_type) { + return; + } + + char stamp[20]; + std::snprintf( + stamp, sizeof(stamp), "%.06lf", + std::chrono::duration_cast>(time.time_since_epoch()).count()); + + try { + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("X-Timestamp", stamp) + .header("Pragma", "no-cache") + .header("Content-type", *content_type) + .header("Access-Control-Allow-Origin", "*") + .header("Content-Length", std::to_string(msg->data.size())) + .write(connection_); + + connection_->write(boost::asio::buffer(msg->data), msg); + } catch (boost::system::system_error & e) { + // happens when client disconnects + RCLCPP_DEBUG(logger_, "system_error exception: %s", e.what()); + inactive_ = true; + return; + } catch (std::exception & e) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception: %s", e.what()); + inactive_ = true; + return; + } catch (...) { + auto & clk = *node->get_clock(); + RCLCPP_ERROR_THROTTLE(logger_, clk, 40, "exception"); + inactive_ = true; + return; + } + + image_sub_.reset(); + inactive_ = true; +} + +void RosCompressedSnapshotStreamer::image_callback( + const sensor_msgs::msg::CompressedImage::ConstSharedPtr msg) +{ + send_image(msg, std::chrono::steady_clock::now()); +} + +std::shared_ptr +RosCompressedSnapshotStreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + auto node_locked = node.lock(); + if (!node_locked) { + RCLCPP_WARN( + rclcpp::get_logger("web_video_server.RosCompressedSnapshotStreamerFactory"), + "Cannot create ROS compressed snapshot streamer because the node has expired"); + return nullptr; + } + + const std::string topic = request.get_query_param_value_or_default("topic", ""); + if (!has_compressed_topic(*node_locked, topic)) { + RCLCPP_WARN( + node_locked->get_logger().get_child("RosCompressedSnapshotStreamerFactory"), + "Could not find compressed image topic for %s, falling back to jpeg", topic.c_str()); + return std::make_shared(request, connection, node); + } + return std::make_shared(request, connection, node); +} + +std::vector RosCompressedSnapshotStreamerFactory::get_available_topics( + rclcpp::Node & node) +{ + return collect_compressed_topics(node); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::RosCompressedStreamerFactory, + web_video_server::StreamerFactoryInterface) +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::RosCompressedSnapshotStreamerFactory, + web_video_server::SnapshotStreamerFactoryInterface) diff --git a/src/streamers/vp8_streamer.cpp b/src/streamers/vp8_streamer.cpp new file mode 100644 index 00000000..82f0df5b --- /dev/null +++ b/src/streamers/vp8_streamer.cpp @@ -0,0 +1,107 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/vp8_streamer.hpp" + +extern "C" +{ +#include +#include +} + +#include +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +Vp8Streamer::Vp8Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: LibavStreamerBase(request, connection, node, "vp8_streamer", "webm", "libvpx", "video/webm") +{ + quality_ = request.get_query_param_value_or_default("quality", "realtime"); +} +Vp8Streamer::~Vp8Streamer() +{ +} + +void Vp8Streamer::initialize_encoder() +{ + typedef std::map AvOptMap; + AvOptMap av_opt_map; + av_opt_map["quality"] = quality_; + av_opt_map["deadline"] = "1"; + av_opt_map["auto-alt-ref"] = "0"; + av_opt_map["lag-in-frames"] = "1"; + av_opt_map["rc_lookahead"] = "1"; + av_opt_map["drop_frame"] = "1"; + av_opt_map["error-resilient"] = "1"; + + for (auto & opt : av_opt_map) { + av_opt_set(codec_context_->priv_data, opt.first.c_str(), opt.second.c_str(), 0); + } + + // Buffering settings + const int bufsize = 10; + codec_context_->rc_buffer_size = bufsize; + codec_context_->rc_initial_buffer_occupancy = bufsize; // bitrate/3; + av_opt_set_int(codec_context_->priv_data, "bufsize", bufsize, 0); + av_opt_set_int(codec_context_->priv_data, "buf-initial", bufsize, 0); + av_opt_set_int(codec_context_->priv_data, "buf-optimal", bufsize, 0); + av_opt_set_int(codec_context_->priv_data, "skip_threshold", 10, 0); +} + +std::shared_ptr Vp8StreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::Vp8StreamerFactory, + web_video_server::StreamerFactoryInterface) diff --git a/src/streamers/vp9_streamer.cpp b/src/streamers/vp9_streamer.cpp new file mode 100644 index 00000000..f2b11878 --- /dev/null +++ b/src/streamers/vp9_streamer.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/streamers/vp9_streamer.hpp" + +extern "C" +{ +#include +#include +} + +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" +#include "rclcpp/node.hpp" + +#include "web_video_server/streamer.hpp" +#include "web_video_server/streamers/libav_streamer.hpp" + +namespace web_video_server +{ +namespace streamers +{ + +Vp9Streamer::Vp9Streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::WeakPtr node) +: LibavStreamerBase(request, connection, node, "vp9_streamer", "webm", "libvpx-vp9", "video/webm") +{ +} +Vp9Streamer::~Vp9Streamer() +{ +} + +void Vp9Streamer::initialize_encoder() +{ + // codec options set up to provide somehow reasonable performance in cost of poor quality + // should be updated as soon as VP9 encoding matures + av_opt_set_int(codec_context_->priv_data, "pass", 1, 0); + av_opt_set_int(codec_context_->priv_data, "speed", 8, 0); + av_opt_set_int(codec_context_->priv_data, "cpu-used", 4, 0); // 8 is max + av_opt_set_int(codec_context_->priv_data, "crf", 20, 0); // 0..63 (higher is lower quality) +} + +std::shared_ptr Vp9StreamerFactory::create_streamer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, + rclcpp::Node::WeakPtr node) +{ + return std::make_shared(request, connection, node); +} + +} // namespace streamers +} // namespace web_video_server + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + web_video_server::streamers::Vp9StreamerFactory, + web_video_server::StreamerFactoryInterface) diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 00000000..80051351 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,55 @@ +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/utils.hpp" + +#include +#include + +#include "rmw/qos_profiles.h" +#include "rmw/types.h" + +namespace web_video_server +{ + +std::optional get_qos_profile_from_name(const std::string name) +{ + if (name == "default") { + return rmw_qos_profile_default; + } + if (name == "system_default") { + return rmw_qos_profile_system_default; + } + if (name == "sensor_data") { + return rmw_qos_profile_sensor_data; + } + return std::nullopt; +} + +} // namespace web_video_server diff --git a/src/vp8_streamer.cpp b/src/vp8_streamer.cpp deleted file mode 100644 index f45b7ca0..00000000 --- a/src/vp8_streamer.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/********************************************************************* - * - * Software License Agreement (BSD License) - * - * Copyright (c) 2014, Worcester Polytechnic Institute - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of the Worcester Polytechnic Institute nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - *********************************************************************/ - -#include "web_video_server/vp8_streamer.h" - -namespace web_video_server -{ - -Vp8Streamer::Vp8Streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - LibavStreamer(request, connection, nh, "webm", "libvpx", "video/webm") -{ - quality_ = request.get_query_param_value_or_default("quality", "realtime"); -} -Vp8Streamer::~Vp8Streamer() -{ -} - -void Vp8Streamer::initializeEncoder() -{ - typedef std::map AvOptMap; - AvOptMap av_opt_map; - av_opt_map["quality"] = quality_; - av_opt_map["deadline"] = "1"; - av_opt_map["auto-alt-ref"] = "0"; - av_opt_map["lag-in-frames"] = "1"; - av_opt_map["rc_lookahead"] = "1"; - av_opt_map["drop_frame"] = "1"; - av_opt_map["error-resilient"] = "1"; - - for (AvOptMap::iterator itr = av_opt_map.begin(); itr != av_opt_map.end(); ++itr) - { - av_opt_set(codec_context_->priv_data, itr->first.c_str(), itr->second.c_str(), 0); - } - - // Buffering settings - int bufsize = 10; - codec_context_->rc_buffer_size = bufsize; - codec_context_->rc_initial_buffer_occupancy = bufsize; //bitrate/3; - av_opt_set_int(codec_context_->priv_data, "bufsize", bufsize, 0); - av_opt_set_int(codec_context_->priv_data, "buf-initial", bufsize, 0); - av_opt_set_int(codec_context_->priv_data, "buf-optimal", bufsize, 0); - av_opt_set_int(codec_context_->priv_data, "skip_threshold", 10, 0); -} - -Vp8StreamerType::Vp8StreamerType() : - LibavStreamerType("webm", "libvpx", "video/webm") -{ -} - -boost::shared_ptr Vp8StreamerType::create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new Vp8Streamer(request, connection, nh)); -} - -} diff --git a/src/vp9_streamer.cpp b/src/vp9_streamer.cpp deleted file mode 100644 index 8fcefd87..00000000 --- a/src/vp9_streamer.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "web_video_server/vp9_streamer.h" - -namespace web_video_server -{ - -Vp9Streamer::Vp9Streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, rclcpp::Node::SharedPtr nh) : - LibavStreamer(request, connection, nh, "webm", "libvpx-vp9", "video/webm") -{ -} -Vp9Streamer::~Vp9Streamer() -{ -} - -void Vp9Streamer::initializeEncoder() -{ - - // codec options set up to provide somehow reasonable performance in cost of poor quality - // should be updated as soon as VP9 encoding matures - av_opt_set_int(codec_context_->priv_data, "pass", 1, 0); - av_opt_set_int(codec_context_->priv_data, "speed", 8, 0); - av_opt_set_int(codec_context_->priv_data, "cpu-used", 4, 0); // 8 is max - av_opt_set_int(codec_context_->priv_data, "crf", 20, 0); // 0..63 (higher is lower quality) -} - -Vp9StreamerType::Vp9StreamerType() : - LibavStreamerType("webm", "libvpx-vp9", "video/webm") -{ -} - -boost::shared_ptr Vp9StreamerType::create_streamer(const async_web_server_cpp::HttpRequest& request, - async_web_server_cpp::HttpConnectionPtr connection, - rclcpp::Node::SharedPtr nh) -{ - return boost::shared_ptr(new Vp9Streamer(request, connection, nh)); -} - -} diff --git a/src/web_video_server.cpp b/src/web_video_server.cpp index c0eebf2f..e1d4076c 100644 --- a/src/web_video_server.cpp +++ b/src/web_video_server.cpp @@ -1,389 +1,430 @@ -#include -#include -#include +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024-2025, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include "web_video_server/web_video_server.hpp" + +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include - -#include "web_video_server/web_video_server.h" -#include "web_video_server/ros_compressed_streamer.h" -#include "web_video_server/jpeg_streamers.h" -#include "web_video_server/png_streamers.h" -#include "web_video_server/vp8_streamer.h" -#include "web_video_server/h264_streamer.h" -#include "web_video_server/vp9_streamer.h" + +#include +#include +#include +#include + +#include "async_web_server_cpp/http_connection.hpp" +#include "async_web_server_cpp/http_request.hpp" #include "async_web_server_cpp/http_reply.hpp" +#include "async_web_server_cpp/http_server.hpp" +#include "pluginlib/exceptions.hpp" +#include "rclcpp/node.hpp" +#include "rclcpp/node_options.hpp" +#include "rclcpp/logging.hpp" + +#include "web_video_server/streamer.hpp" using namespace std::chrono_literals; -using namespace boost::placeholders; +using namespace boost::placeholders; // NOLINT namespace web_video_server { -static bool __verbose; - -static std::string __default_stream_type; - -static bool ros_connection_logger(async_web_server_cpp::HttpServerRequestHandler forward, - const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, - const char* end) +WebVideoServer::WebVideoServer(const rclcpp::NodeOptions & options) +: rclcpp::Node("web_video_server", options), handler_group_( + async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)), + streamer_factory_loader_("web_video_server", "web_video_server::StreamerFactoryInterface"), + snapshot_streamer_factory_loader_("web_video_server", + "web_video_server::SnapshotStreamerFactoryInterface") { - if (__verbose) - { - // TODO reenable - // RCLCPP_INFO(nh->get_logger(), "Handling Request: %s", request.uri.c_str()); - } - try - { - forward(request, connection, begin, end); - return true; - } - catch (std::exception &e) - { - // TODO reenable - // RCLCPP_WARN(nh->get_logger(), "Error Handling Request: %s", e.what()); - return false; + declare_parameter("port", 8080); + declare_parameter("verbose", true); + declare_parameter("address", "0.0.0.0"); + declare_parameter("server_threads", 1); + declare_parameter("publish_rate", -1.0); + declare_parameter("default_stream_type", "mjpeg"); + declare_parameter("default_snapshot_type", "jpeg"); + + get_parameter("port", port_); + get_parameter("verbose", verbose_); + get_parameter("address", address_); + int server_threads; + get_parameter("server_threads", server_threads); + get_parameter("publish_rate", publish_rate_); + get_parameter("default_stream_type", default_stream_type_); + get_parameter("default_snapshot_type", default_snapshot_type_); + + for (auto cls : streamer_factory_loader_.getDeclaredClasses()) { + RCLCPP_INFO(get_logger(), "Loading streamer plugin: %s", cls.c_str()); + try { + auto streamer = streamer_factory_loader_.createSharedInstance(cls); + streamer_factories_[streamer->get_type()] = streamer; + } catch (pluginlib::PluginlibException & ex) { + RCLCPP_ERROR(get_logger(), "The plugin failed to load for some reason. Error: %s", ex.what()); + } } - return false; -} -WebVideoServer::WebVideoServer(rclcpp::Node::SharedPtr &nh, rclcpp::Node::SharedPtr &private_nh) : - nh_(nh), handler_group_( - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)) -{ - rclcpp::Parameter parameter; - if (private_nh->get_parameter("port", parameter)) { - port_ = parameter.as_int(); - } else { - port_ = 8080; - } - if (private_nh->get_parameter("verbose", parameter)) { - __verbose = parameter.as_bool(); - } else { - __verbose = true; + for (auto cls : snapshot_streamer_factory_loader_.getDeclaredClasses()) { + RCLCPP_INFO(get_logger(), "Loading streamer plugin: %s", cls.c_str()); + try { + auto streamer = snapshot_streamer_factory_loader_.createSharedInstance(cls); + snapshot_streamer_factories_[streamer->get_type()] = streamer; + } catch (pluginlib::PluginlibException & ex) { + RCLCPP_ERROR(get_logger(), "The plugin failed to load for some reason. Error: %s", ex.what()); + } } - if (private_nh->get_parameter("address", parameter)) { - address_ = parameter.as_string(); - } else { - address_ = "0.0.0.0"; + handler_group_.addHandlerForPath( + "/", + boost::bind(&WebVideoServer::handle_list_streams, this, _1, _2, _3, _4)); + handler_group_.addHandlerForPath( + "/stream", + boost::bind(&WebVideoServer::handle_stream, this, _1, _2, _3, _4)); + handler_group_.addHandlerForPath( + "/stream_viewer", + boost::bind(&WebVideoServer::handle_stream_viewer, this, _1, _2, _3, _4)); + handler_group_.addHandlerForPath( + "/snapshot", + boost::bind(&WebVideoServer::handle_snapshot, this, _1, _2, _3, _4)); + handler_group_.addHandlerForPath( + "/shutdown", + boost::bind(&WebVideoServer::handle_shutdown, this, _1, _2, _3, _4)); + + try { + server_.reset( + new async_web_server_cpp::HttpServer( + address_, std::to_string(port_), + boost::bind(&WebVideoServer::handle_request, this, _1, _2, _3, _4), + server_threads + ) + ); + } catch (boost::exception & e) { + RCLCPP_ERROR( + get_logger(), "Exception when creating the web server! %s:%d", + address_.c_str(), port_); + throw; } - int server_threads; - if (private_nh->get_parameter("server_threads", parameter)) { - server_threads = parameter.as_int(); - } else { - server_threads = 1; - } + RCLCPP_INFO(get_logger(), "Waiting For connections on %s:%d", address_.c_str(), port_); - if (private_nh->get_parameter("ros_threads", parameter)) { - ros_threads_ = parameter.as_int(); - } else { - ros_threads_ = 2; - } - if (private_nh->get_parameter("publish_rate", parameter)) { - publish_rate_ = parameter.as_double(); - } else { - publish_rate_ = -1.0; + if (publish_rate_ > 0) { + restream_timer_ = create_wall_timer( + 1s / publish_rate_, + [this]() {restream_frames(1s / publish_rate_);}); } - if (private_nh->get_parameter("default_stream_type", parameter)) { - __default_stream_type = parameter.as_string(); - } else { - __default_stream_type = "mjpeg"; - } - - stream_types_["mjpeg"] = boost::shared_ptr(new MjpegStreamerType()); - stream_types_["png"] = boost::shared_ptr(new PngStreamerType()); - stream_types_["ros_compressed"] = boost::shared_ptr(new RosCompressedStreamerType()); - stream_types_["vp8"] = boost::shared_ptr(new Vp8StreamerType()); - stream_types_["h264"] = boost::shared_ptr(new H264StreamerType()); - stream_types_["vp9"] = boost::shared_ptr(new Vp9StreamerType()); + cleanup_timer_ = create_wall_timer(500ms, [this]() {cleanup_inactive_streams();}); - handler_group_.addHandlerForPath("/", boost::bind(&WebVideoServer::handle_list_streams, this, _1, _2, _3, _4)); - handler_group_.addHandlerForPath("/stream", boost::bind(&WebVideoServer::handle_stream, this, _1, _2, _3, _4)); - handler_group_.addHandlerForPath("/stream_viewer", - boost::bind(&WebVideoServer::handle_stream_viewer, this, _1, _2, _3, _4)); - handler_group_.addHandlerForPath("/snapshot", boost::bind(&WebVideoServer::handle_snapshot, this, _1, _2, _3, _4)); - - try - { - server_.reset( - new async_web_server_cpp::HttpServer(address_, boost::lexical_cast(port_), - boost::bind(ros_connection_logger, handler_group_, _1, _2, _3, _4), - server_threads)); - } - catch(boost::exception& e) - { - RCLCPP_ERROR(nh_->get_logger(), "Exception when creating the web server! %s:%d", address_.c_str(), port_); - throw; - } + server_->run(); } WebVideoServer::~WebVideoServer() { + server_->stop(); } -void WebVideoServer::setup_cleanup_inactive_streams() +void WebVideoServer::restream_frames(std::chrono::duration max_age) { - std::function callback = std::bind(&WebVideoServer::cleanup_inactive_streams, this); - cleanup_timer_ = nh_->create_wall_timer(500ms, callback); -} + const std::scoped_lock lock(streamers_mutex_); -void WebVideoServer::spin() -{ - server_->run(); - RCLCPP_INFO(nh_->get_logger(), "Waiting For connections on %s:%d", address_.c_str(), port_); - rclcpp::executors::MultiThreadedExecutor spinner(rclcpp::ExecutorOptions(), ros_threads_); - spinner.add_node(nh_); - if ( publish_rate_ > 0 ) { - nh_->create_wall_timer(1s / publish_rate_, [this](){restreamFrames(1.0 / publish_rate_);}); + for (auto & streamer : streamers_) { + streamer->restream_frame(max_age); } - spinner.spin(); - server_->stop(); } -void WebVideoServer::restreamFrames( double max_age ) +void WebVideoServer::cleanup_inactive_streams() { - boost::mutex::scoped_lock lock(subscriber_mutex_); - - typedef std::vector >::iterator itr_type; - - for (itr_type itr = image_subscribers_.begin(); itr < image_subscribers_.end(); ++itr) - { - (*itr)->restreamFrame( max_age ); + const std::unique_lock lock(streamers_mutex_, std::try_to_lock); + if (lock) { + auto new_end = std::partition( + streamers_.begin(), streamers_.end(), + [](const std::shared_ptr & streamer) {return !streamer->is_inactive();}); + if (verbose_) { + for (auto itr = new_end; itr < streamers_.end(); ++itr) { + RCLCPP_INFO(get_logger(), "Removed Stream: %s", (*itr)->get_topic().c_str()); + } } + streamers_.erase(new_end, streamers_.end()); + } } -void WebVideoServer::cleanup_inactive_streams() +bool WebVideoServer::handle_request( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, const char * begin, + const char * end) { - boost::mutex::scoped_lock lock(subscriber_mutex_, boost::try_to_lock); - if (lock) - { - typedef std::vector >::iterator itr_type; - itr_type new_end = std::partition(image_subscribers_.begin(), image_subscribers_.end(), - !boost::bind(&ImageStreamer::isInactive, _1)); - if (__verbose) - { - for (itr_type itr = new_end; itr < image_subscribers_.end(); ++itr) - { - RCLCPP_INFO(nh_->get_logger(), "Removed Stream: %s", (*itr)->getTopic().c_str()); - } - } - image_subscribers_.erase(new_end, image_subscribers_.end()); + if (verbose_) { + RCLCPP_INFO(get_logger(), "Handling Request: %s", request.uri.c_str()); + } + + try { + return handler_group_(request, connection, begin, end); + } catch (std::exception & e) { + RCLCPP_WARN(get_logger(), "Error Handling Request: %s", e.what()); + return false; } } -bool WebVideoServer::handle_stream(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, - const char* end) +bool WebVideoServer::handle_stream( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, const char * begin, + const char * end) { - std::string type = request.get_query_param_value_or_default("type", __default_stream_type); - if (stream_types_.find(type) != stream_types_.end()) - { - std::string topic = request.get_query_param_value_or_default("topic", ""); - // Fallback for topics without corresponding compressed topics - if (type == std::string("ros_compressed")) - { - std::string compressed_topic_name = topic + "/compressed"; - auto tnat = nh_->get_topic_names_and_types(); - bool did_find_compressed_topic = false; - for (auto topic_and_types : tnat) { - if (topic_and_types.second.size() > 1) { - // explicitly avoid topics with more than one type - break; - } - auto & topic_name = topic_and_types.first; - if(topic_name == compressed_topic_name || (topic_name.find("/") == 0 && topic_name.substr(1) == compressed_topic_name)){ - did_find_compressed_topic = true; - break; - } - } - if (!did_find_compressed_topic) - { - RCLCPP_WARN(nh_->get_logger(), "Could not find compressed image topic for %s, falling back to mjpeg", topic.c_str()); - type = "mjpeg"; - } - } - boost::shared_ptr streamer = stream_types_[type]->create_streamer(request, connection, nh_); + const std::string type = request.get_query_param_value_or_default("type", default_stream_type_); + if (streamer_factories_.find(type) != streamer_factories_.end()) { + const std::shared_ptr streamer = streamer_factories_[type]->create_streamer( + request, connection, weak_from_this()); streamer->start(); - boost::mutex::scoped_lock lock(subscriber_mutex_); - image_subscribers_.push_back(streamer); - } - else - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)(request, connection, begin, - end); + const std::scoped_lock lock(streamers_mutex_); + streamers_.push_back(streamer); + } else { + async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)( + request, connection, begin, end); } return true; } -bool WebVideoServer::handle_snapshot(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, - const char* end) +bool WebVideoServer::handle_snapshot( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, const char * begin, + const char * end) { - boost::shared_ptr streamer(new JpegSnapshotStreamer(request, connection, nh_)); - streamer->start(); - - boost::mutex::scoped_lock lock(subscriber_mutex_); - image_subscribers_.push_back(streamer); + const std::string type = request.get_query_param_value_or_default("type", default_snapshot_type_); + if (snapshot_streamer_factories_.find(type) != snapshot_streamer_factories_.end()) { + const std::shared_ptr streamer = + snapshot_streamer_factories_[type]->create_streamer( + request, connection, weak_from_this()); + streamer->start(); + const std::scoped_lock lock(streamers_mutex_); + streamers_.push_back(streamer); + } else { + async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)( + request, connection, begin, end); + } return true; } -bool WebVideoServer::handle_stream_viewer(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, - const char* end) +bool WebVideoServer::handle_stream_viewer( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, const char * begin, + const char * end) { - std::string type = request.get_query_param_value_or_default("type", __default_stream_type); - if (stream_types_.find(type) != stream_types_.end()) - { - std::string topic = request.get_query_param_value_or_default("topic", ""); - // Fallback for topics without corresponding compressed topics - if (type == std::string("ros_compressed")) - { - - std::string compressed_topic_name = topic + "/compressed"; - auto tnat = nh_->get_topic_names_and_types(); - bool did_find_compressed_topic = false; - for (auto topic_and_types : tnat) { - if (topic_and_types.second.size() > 1) { - // explicitly avoid topics with more than one type - break; - } - auto & topic_name = topic_and_types.first; - if(topic_name == compressed_topic_name || (topic_name.find("/") == 0 && topic_name.substr(1) == compressed_topic_name)){ - did_find_compressed_topic = true; - break; - } - } - if (!did_find_compressed_topic) - { - RCLCPP_WARN(nh_->get_logger(), "Could not find compressed image topic for %s, falling back to mjpeg", topic.c_str()); - type = "mjpeg"; - } - } + const std::string type = request.get_query_param_value_or_default("type", default_stream_type_); + if (streamer_factories_.find(type) != streamer_factories_.end()) { + const std::string topic = request.get_query_param_value_or_default("topic", ""); - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok).header("Connection", "close").header( - "Server", "web_video_server").header("Content-type", "text/html;").write(connection); + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header("Content-type", "text/html;") + .write(connection); std::stringstream ss; ss << "" << topic << ""; ss << "

" << topic << "

"; - ss << stream_types_[type]->create_viewer(request); + ss << streamer_factories_[type]->create_viewer(request); ss << ""; connection->write(ss.str()); - } - else - { - async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)(request, connection, begin, - end); + } else { + async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::not_found)( + request, connection, begin, end); } return true; } -bool WebVideoServer::handle_list_streams(const async_web_server_cpp::HttpRequest &request, - async_web_server_cpp::HttpConnectionPtr connection, const char* begin, - const char* end) +bool WebVideoServer::handle_shutdown( + const async_web_server_cpp::HttpRequest & request, + async_web_server_cpp::HttpConnectionPtr connection, const char * begin, + const char * end) { - std::vector image_topics; - std::vector camera_info_topics; - auto tnat = nh_->get_topic_names_and_types(); - for (auto topic_and_types : tnat) { - if (topic_and_types.second.size() > 1) { - // explicitly avoid topics with more than one type - break; - } - auto & topic_name = topic_and_types.first; - auto & topic_type = topic_and_types.second[0]; // explicitly take the first - // TODO debugging - fprintf(stderr, "topic_type: %s\n", topic_type.c_str()); - if (topic_type == "sensor_msgs/msg/Image") - { - image_topics.push_back(topic_name); - } - else if (topic_type == "sensor_msgs/msg/CameraInfo") - { - camera_info_topics.push_back(topic_name); - } + const std::string topic = request.get_query_param_value_or_default("topic", ""); + if (topic.empty()) { + async_web_server_cpp::HttpReply::stock_reply(async_web_server_cpp::HttpReply::bad_request)( + request, connection, begin, end); + return true; } - async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok).header("Connection", "close").header( - "Server", "web_video_server").header("Cache-Control", - "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0").header( - "Pragma", "no-cache").header("Content-type", "text/html;").write(connection); + const std::string client_id = request.get_query_param_value_or_default("client_id", ""); - connection->write("" - "ROS Image Topic List" - "

Available ROS Image Topics:

"); - connection->write("
    "); - BOOST_FOREACH(std::string & camera_info_topic, camera_info_topics) + int stopped = 0; { - if (boost::algorithm::ends_with(camera_info_topic, "/camera_info")) - { - std::string base_topic = camera_info_topic.substr(0, camera_info_topic.size() - strlen("camera_info")); - connection->write("
  • "); - connection->write(base_topic); - connection->write(""); } - connection->write("
  • "); } - connection->write("
"); - // Add the rest of the image topics that don't have camera_info. - connection->write(""); - return true; -} + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header("Content-type", "text/plain;") + .write(connection); + + connection->write("stopped=" + std::to_string(stopped)); + return true; } -int main(int argc, char **argv) +bool WebVideoServer::handle_list_streams( + const async_web_server_cpp::HttpRequest & /* request */, + async_web_server_cpp::HttpConnectionPtr connection, const char * /* begin */, + const char * /* end */) { - rclcpp::init(argc, argv); - auto nh = std::make_shared("web_video_server"); - auto private_nh = std::make_shared("_web_video_server"); + std::map> topics_by_streamer_type; + std::map> topics_by_snapshot_type; + std::set all_topics; + + for (const auto & factory_pair : streamer_factories_) { + RCLCPP_DEBUG(get_logger(), "Getting topics from factory: %s", factory_pair.first.c_str()); + const std::vector factory_topics = + factory_pair.second->get_available_topics(*this); + RCLCPP_DEBUG( + get_logger(), "Factory %s returned %zu topics", + factory_pair.first.c_str(), factory_topics.size()); + for (const auto & topic : factory_topics) { + RCLCPP_DEBUG(get_logger(), " Topic: %s", topic.c_str()); + topics_by_streamer_type[factory_pair.first].push_back(topic); + all_topics.insert(topic); + } + } - web_video_server::WebVideoServer server(nh, private_nh); - server.setup_cleanup_inactive_streams(); - server.spin(); + for (const auto & factory_pair : snapshot_streamer_factories_) { + RCLCPP_DEBUG(get_logger(), "Getting topics from factory: %s", factory_pair.first.c_str()); + const std::vector factory_topics = + factory_pair.second->get_available_topics(*this); + RCLCPP_DEBUG( + get_logger(), "Factory %s returned %zu topics", + factory_pair.first.c_str(), factory_topics.size()); + for (const auto & topic : factory_topics) { + RCLCPP_DEBUG(get_logger(), " Topic: %s", topic.c_str()); + topics_by_snapshot_type[factory_pair.first].push_back(topic); + all_topics.insert(topic); + } + } - return (0); + async_web_server_cpp::HttpReply::builder(async_web_server_cpp::HttpReply::ok) + .header("Connection", "close") + .header("Server", "web_video_server") + .header( + "Cache-Control", + "no-cache, no-store, must-revalidate, pre-check=0, post-check=0, max-age=0") + .header("Pragma", "no-cache").header("Content-type", "text/html;").write(connection); + + connection->write( + "" + "ROS Streamable Topic List" + "

Available ROS Topics for streaming:

"); + connection->write("
    "); + for (const std::string & topic : all_topics) { + std::vector available_stream_viewers; + std::vector available_streams; + std::vector available_snapshots; + + for (const auto & factory_pair : topics_by_streamer_type) { + const auto & type = factory_pair.first; + const auto & topics = factory_pair.second; + if (std::find(topics.begin(), topics.end(), topic) != topics.end()) { + available_stream_viewers.push_back( + "" + type + ""); + available_streams.push_back( + "" + type + ""); + } + } + + for (const auto & factory_pair : topics_by_snapshot_type) { + const auto & type = factory_pair.first; + const auto & topics = factory_pair.second; + if (std::find(topics.begin(), topics.end(), topic) != topics.end()) { + available_snapshots.push_back( + "" + type + ""); + } + } + + connection->write("
  • "); + connection->write(topic); + connection->write("
      "); + if (!available_streams.empty()) { + connection->write("
    • "); + connection->write(""); + connection->write("Stream Viewer ("); + connection->write(boost::algorithm::join(available_stream_viewers, ", ")); + connection->write(")"); + connection->write("
    • "); + connection->write("
    • "); + connection->write(""); + connection->write("Stream ("); + connection->write(boost::algorithm::join(available_streams, ", ")); + connection->write(")"); + connection->write("
    • "); + } + if (!available_snapshots.empty()) { + connection->write("
    • "); + connection->write(""); + connection->write("Snapshot ("); + connection->write(boost::algorithm::join(available_snapshots, ", ")); + connection->write(")"); + connection->write("
    • "); + } + connection->write("
    "); + connection->write("
  • "); + } + connection->write("
"); + return true; } + +} // namespace web_video_server + +#include "rclcpp_components/register_node_macro.hpp" + +// Register the component with class_loader. +// This acts as a sort of entry point, allowing the component to be discoverable when its library +// is being loaded into a running process. +RCLCPP_COMPONENTS_REGISTER_NODE(web_video_server::WebVideoServer) diff --git a/src/web_video_server_node.cpp b/src/web_video_server_node.cpp new file mode 100644 index 00000000..53342595 --- /dev/null +++ b/src/web_video_server_node.cpp @@ -0,0 +1,51 @@ +// Copyright (c) 2014, Worcester Polytechnic Institute +// Copyright (c) 2024, The Robot Web Tools Contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#include + +#include "rclcpp/executors/multi_threaded_executor.hpp" +#include "rclcpp/utilities.hpp" + +#include "web_video_server/web_video_server.hpp" + +int main(int argc, char ** argv) +{ + rclcpp::init(argc, argv); + auto node = std::make_shared(rclcpp::NodeOptions()); + + node->declare_parameter("ros_threads", 2); + int ros_threads{2}; + node->get_parameter("ros_threads", ros_threads); + rclcpp::executors::MultiThreadedExecutor spinner(rclcpp::ExecutorOptions(), ros_threads); + spinner.add_node(node); + spinner.spin(); + + return 0; +}