├── .clang-format ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc ├── conf.py ├── index.rst └── readme_include.md ├── ffmpeg_image_transport.repos ├── ffmpeg_plugins.xml ├── include └── ffmpeg_image_transport │ ├── ffmpeg_publisher.hpp │ └── ffmpeg_subscriber.hpp ├── launch └── cam.launch.py ├── package.xml ├── rosdoc2.yaml └── src ├── ffmpeg_publisher.cpp ├── ffmpeg_subscriber.cpp └── manifest.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Google 4 | 5 | AccessModifierOffset: -2 6 | AlignAfterOpenBracket: AlwaysBreak 7 | BraceWrapping: 8 | AfterClass: true 9 | AfterFunction: true 10 | AfterNamespace: true 11 | AfterStruct: true 12 | BreakBeforeBraces: Custom 13 | ColumnLimit: 100 14 | ConstructorInitializerIndentWidth: 0 15 | ContinuationIndentWidth: 2 16 | DerivePointerAlignment: false 17 | PointerAlignment: Middle 18 | ReflowComments: false 19 | ... 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # continuous integration workflow 3 | # 4 | name: build repo 5 | 6 | on: 7 | push: 8 | branches: [ master] 9 | pull_request: 10 | branches: [ master] 11 | workflow_dispatch: 12 | branches: [ master] 13 | 14 | jobs: 15 | build_ros2: 16 | uses: ros-misc-utilities/ros_build_scripts/.github/workflows/ros2_recent_ci.yml@master 17 | with: 18 | repo: ${{ github.event.repository.name }} 19 | vcs_url: https://raw.githubusercontent.com/${{ github.repository }}/master/${{ github.event.repository.name }}.repos 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/generated 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(ffmpeg_image_transport) 4 | 5 | # Default to C++14 6 | if(NOT CMAKE_CXX_STANDARD) 7 | set(CMAKE_CXX_STANDARD 14) 8 | endif() 9 | 10 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 11 | add_compile_options(-Wall -Wextra -Wpedantic -Werror) 12 | endif() 13 | 14 | # the image transport api changed between distros 15 | if(DEFINED ENV{ROS_DISTRO}) 16 | if($ENV{ROS_DISTRO} STREQUAL "foxy" OR 17 | $ENV{ROS_DISTRO} STREQUAL "galactic") 18 | add_definitions(-DIMAGE_TRANSPORT_API_V1) 19 | elseif($ENV{ROS_DISTRO} STREQUAL "humble") 20 | add_definitions(-DIMAGE_TRANSPORT_API_V2) 21 | else() 22 | add_definitions(-DIMAGE_TRANSPORT_API_V3) 23 | endif() 24 | else() 25 | message(ERROR "ROS_DISTRO environment variable is not set!") 26 | endif() 27 | 28 | find_package(ament_cmake REQUIRED) 29 | find_package(ament_cmake_ros REQUIRED) 30 | find_package(ffmpeg_image_transport_msgs REQUIRED) 31 | find_package(ffmpeg_encoder_decoder REQUIRED) 32 | find_package(image_transport REQUIRED) 33 | find_package(pluginlib REQUIRED) 34 | find_package(rclcpp REQUIRED) 35 | find_package(rcutils REQUIRED) 36 | find_package(sensor_msgs REQUIRED) 37 | find_package(std_msgs REQUIRED) 38 | 39 | 40 | set(LIBRARY_NAME ${PROJECT_NAME}_component) 41 | 42 | add_library( 43 | ${LIBRARY_NAME} 44 | SHARED 45 | src/ffmpeg_publisher.cpp 46 | src/ffmpeg_subscriber.cpp 47 | src/manifest.cpp) 48 | 49 | target_include_directories(${LIBRARY_NAME} PUBLIC 50 | "$" 51 | "$") 52 | 53 | target_link_libraries(${LIBRARY_NAME} 54 | PUBLIC 55 | image_transport::image_transport 56 | pluginlib::pluginlib 57 | rclcpp::rclcpp 58 | ${sensor_msgs_TARGETS} 59 | ffmpeg_encoder_decoder::ffmpeg_encoder_decoder 60 | ${ffmpeg_image_transport_msgs_TARGETS}) 61 | 62 | ament_export_dependencies( 63 | image_transport 64 | pluginlib 65 | rclcpp 66 | rcutils 67 | sensor_msgs 68 | std_msgs 69 | ffmpeg_encoder_decoder 70 | ffmpeg_image_transport_msgs) 71 | 72 | install(TARGETS ${LIBRARY_NAME} 73 | EXPORT export_${LIBRARY_NAME} 74 | ARCHIVE DESTINATION lib 75 | LIBRARY DESTINATION lib 76 | RUNTIME DESTINATION bin 77 | INCLUDES DESTINATION include) 78 | 79 | ament_export_include_directories(include) 80 | ament_export_libraries(${LIBRARY_NAME}) 81 | ament_export_targets(export_${LIBRARY_NAME} HAS_LIBRARY_TARGET) 82 | 83 | install(DIRECTORY 84 | launch 85 | DESTINATION share/${PROJECT_NAME}/ 86 | FILES_MATCHING PATTERN "*.py") 87 | 88 | install( 89 | DIRECTORY include/ 90 | DESTINATION include 91 | ) 92 | 93 | pluginlib_export_plugin_description_file(image_transport ffmpeg_plugins.xml) 94 | 95 | if(BUILD_TESTING) 96 | find_package(ament_cmake REQUIRED) 97 | find_package(ament_cmake_copyright REQUIRED) 98 | find_package(ament_cmake_cppcheck REQUIRED) 99 | find_package(ament_cmake_cpplint REQUIRED) 100 | find_package(ament_cmake_flake8 REQUIRED) 101 | find_package(ament_cmake_lint_cmake REQUIRED) 102 | find_package(ament_cmake_clang_format REQUIRED) 103 | find_package(ament_cmake_xmllint REQUIRED) 104 | 105 | if(NOT $ENV{ROS_DISTRO} STREQUAL "galactic") 106 | find_package(ament_cmake_pep257 REQUIRED) 107 | endif() 108 | 109 | ament_copyright() 110 | ament_cppcheck(LANGUAGE c++) 111 | ament_cpplint(FILTERS "-build/include,-runtime/indentation_namespace") 112 | ament_flake8() 113 | ament_lint_cmake() 114 | if(NOT $ENV{ROS_DISTRO} STREQUAL "galactic") 115 | ament_pep257() 116 | endif() 117 | ament_clang_format(CONFIG_FILE .clang-format) 118 | ament_xmllint() 119 | endif() 120 | 121 | ament_package() 122 | 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Any contribution that you make to this repository will 2 | be under the Apache 2 License, as dictated by that 3 | [license](http://www.apache.org/licenses/LICENSE-2.0.html): 4 | 5 | ~~~ 6 | 5. Submission of Contributions. Unless You explicitly state otherwise, 7 | any Contribution intentionally submitted for inclusion in the Work 8 | by You to the Licensor shall be under the terms and conditions of 9 | this License, without any additional terms or conditions. 10 | Notwithstanding the above, nothing herein shall supersede or modify 11 | the terms of any separate license agreement you may have executed 12 | with Licensor regarding such Contributions. 13 | ~~~ 14 | 15 | Contributors must sign-off each commit by adding a `Signed-off-by: ...` 16 | line to commit messages to certify that they have the right to submit 17 | the code they are contributing to the project according to the 18 | [Developer Certificate of Origin (DCO)](https://developercertificate.org/). 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ROS2 image transport for ffmpeg/libav 2 | 3 | This ROS2 image transport plugin supports encoding/decoding with the FFMpeg 4 | library, for example encoding h264 and h265 or HEVC, using 5 | Nvidia or other hardware acceleration when available. 6 | 7 | The publisher plugin of the transport produces 8 | [ffmpeg image transport messages](https://github.com/ros-misc-utilities/ffmpeg_image_transport_msgs/). 9 | These are raw, encoded packets that are then transmitted and decoded by the 10 | subscriber plugin of the transport. The transport library 11 | contains both the publisher(encoder) and subscriber(decoder) plugin 12 | and therefore must be installed on both sides to be useful. 13 | 14 | To extract e.g. frames or an mp4 file from a recorded bag, have a look at the 15 | [ffmpeg\_image\_transport\_tools](https://github.com/ros-misc-utilities/ffmpeg_image_transport_tools) repository. 16 | 17 | ## Supported systems 18 | 19 | Continuous integration is tested under Ubuntu with the following ROS2 distros: 20 | 21 | [![Build Status](https://build.ros2.org/buildStatus/icon?job=Hdev__ffmpeg_image_transport__ubuntu_jammy_amd64&subject=Humble)](https://build.ros2.org/job/Hdev__ffmpeg_image_transport__ubuntu_jammy_amd64/) 22 | [![Build Status](https://build.ros2.org/buildStatus/icon?job=Jdev__ffmpeg_image_transport__ubuntu_noble_amd64&subject=Jazzy)](https://build.ros2.org/job/Jdev__ffmpeg_image_transport__ubuntu_noble_amd64/) 23 | [![Build Status](https://build.ros2.org/buildStatus/icon?job=Rdev__ffmpeg_image_transport__ubuntu_noble_amd64&subject=Rolling)](https://build.ros2.org/job/Rdev__ffmpeg_image_transport__ubuntu_noble_amd64/) 24 | 25 | 26 | ## Installation 27 | 28 | ### From packages 29 | 30 | ```bash 31 | sudo apt-get install ros-${ROS_DISTRO}-ffmpeg-image-transport 32 | ``` 33 | 34 | ### From source 35 | 36 | Set the following shell variables: 37 | ```bash 38 | repo=ffmpeg_image_transport 39 | url=https://github.com/ros-misc-utilities/${repo}.git 40 | ``` 41 | and follow the [instructions here](https://github.com/ros-misc-utilities/.github/blob/master/docs/build_ros_repository.md) 42 | 43 | Make sure to source your workspace's ``install/setup.bash`` afterwards. 44 | If all goes well you should see the transport show up: 45 | 46 | ``` 47 | ros2 run image_transport list_transports 48 | ``` 49 | 50 | should give output (among other transport plugins): 51 | 52 | ```text 53 | "image_transport/ffmpeg" 54 | - Provided by package: ffmpeg_image_transport 55 | - Publisher: 56 | This plugin encodes frames into ffmpeg compressed packets 57 | 58 | - Subscriber: 59 | This plugin decodes frames from ffmpeg compressed packets 60 | ``` 61 | 62 | Remember to install the plugin on both hosts, the one that is encoding and 63 | the one that is decoding (viewing). 64 | 65 | ## Parameters 66 | 67 | ### Publisher (camera driver) 68 | 69 | Here is a list of the available encoding parameters: 70 | 71 | - ``encoding``: the libav (ffmpeg) encoder being used. The default is ``libx264``, which is on-CPU unaccelerated encoding. 72 | Depending on your hardware, your encoding options may include the hardware accelerated ``h264_nvenc`` or ``h264_vaapi``. 73 | You can list all available encoders with ``ffmpeg --codecs``. In the h264 row, look for ``(encoders)``. 74 | - ``preset``: default is empty (""). Valid values can be for instance ``slow``, ``ll`` (low latency) etc. 75 | To find out what presets are available, run e.g. 76 | ``ffmpeg -hide_banner -f lavfi -i nullsrc -c:v libx264 -preset help -f mp4 - 2>&1`` 77 | - ``profile``: For instance ``baseline``, ``main``. See [the ffmpeg website](https://trac.ffmpeg.org/wiki/Encode/H.264). 78 | - ``tune``: See [the ffmpeg website](https://trac.ffmpeg.org/wiki/Encode/H.264). The default is empty(""). 79 | - ``gop_size``: The number of frames between keyframes. Default: 10. 80 | The larger this number the more latency you will have, but also the more efficient the compression becomes. 81 | - ``bit_rate``: The max bit rate [in bits/s] that the encoding will target. Default is ``8242880``. 82 | - ``crf``: Constant Rate Factor, affects the image quality. Value range is ``[0, 51]``; ``0`` is lossless, ``23`` is default, ``51`` is worst quality. 83 | - ``delay``: Not sure what it does, but doesn't help with delay. Default is empty (""). 84 | - ``pixel_format``: Forces a different pixel format for internal conversions. Experimental, don't use. 85 | - ``qmax``: Max quantization rate. Defaults to 10. See [ffmpeg documentation](https://www.ffmpeg.org/ffmpeg-codecs.html). 86 | The larger this number, the worse the image looks, and the more efficient the encoding. 87 | - ``measure_performance``: For performance debugging (developers only). Defaults to false. 88 | - ``performance_interval``: How many frames to wait between logging performance data. 89 | 90 | The parameters are under the ``ffmpeg`` variable block. If you launch 91 | your publisher node (camera driver), you can give it a parameter list on the way like so: 92 | ``` 93 | parameters=[{'ffmpeg_image_transport.encoding': 'hevc_nvenc', 94 | 'ffmpeg_image_transport.profile': 'main', 95 | 'ffmpeg_image_transport.preset': 'll', 96 | 'ffmpeg_image_transport.gop_size': 15}] 97 | ``` 98 | See the example launch file for a V4L USB camera 99 | 100 | ### Subscriber (viewer) 101 | 102 | The subscriber has only one parameter (``map``), which is the map between the encoding that 103 | was used to encode the frames, and the libav decoder to be used for decoding. The mapping is done by creating entries in the ``ffmpeg.map`` parameter, which is prefixed by the image base name, e.g. ``camera``. 104 | 105 | For example to tell the subscriber to use the ``hevc`` decoder instead of the default ``hevc_cuvid`` 106 | decoder for decoding incoming ``hevc_nvenc`` packets set a parameter like so *after* you started the viewer: 107 | ``` 108 | ros2 param set camera.image_raw.ffmpeg.map.hevc_nvenc hevc 109 | ``` 110 | This is assuming that your viewer node is subscribing to an image ``/camera/image_raw/ffmpeg``. 111 | 112 | You also need to refresh the subscription (drop down menu in the viewer) for the parameter to take hold. 113 | If anyone ever figures out how to set the parameters *when* starting the viewer (rather than afterwards!), please update this document. 114 | 115 | 116 | ### Republishing 117 | 118 | The ``image_transport`` allows you to republish the decoded image locally, 119 | see for instance [here](https://gitlab.com/boldhearts/ros2_v4l2_camera/-/blob/foxy/README.md). 120 | Here the ROS parameters work as expected to modify the mapping between 121 | encoding and decoder. 122 | 123 | The following lines shows how to specify the decoder when republishing. 124 | For example to decode incoming ``hevc_nvenc`` packets with the ``hevc`` decoder: 125 | 126 | - ROS 2 Humble: 127 | ``` 128 | ros2 run image_transport republish ffmpeg in/ffmpeg:=image_raw/ffmpeg raw out:=image_raw/uncompressed --ros-args -p "ffmpeg_image_transport.map.hevc_nvenc:=hevc" 129 | ``` 130 | - ROS 2 Jazzy: 131 | ``` 132 | ros2 run image_transport republish --ros-args -p in_transport:=ffmpeg -p out_transport:=raw --remap in/ffmpeg:=image_raw/ffmpeg --remap out:=image_raw/uncompressed -p "ffmpeg_image_transport.map.hevc_nvenc:=hevc" 133 | ``` 134 | 135 | Note: The commands below use the Humble syntax and need to be changed as shown here for Jazzy. 136 | 137 | Republishing is generally not necessary so long as publisher and subscriber both properly use 138 | an image transport. Some nodes however, notably the rosbag player, do not support a proper transport, rendering republishing necessary. 139 | 140 | #### Republishing raw images from rosbags in ffmpeg format 141 | 142 | Suppose you have raw images in a rosbag but want to play them across a network using 143 | the ``ffmpeg_image_transport``. In this case run a republish node like this 144 | (assuming your rosbag topic is ``/camera/image_raw``): 145 | ``` 146 | ros2 run image_transport republish raw in:=/camera/image_raw 147 | ``` 148 | The republished topic will be under a full transport, meaning you can now view them with e.g. ``rqt_image_view`` under the topic ``/out/ffmpeg``. 149 | 150 | You can record them in ``ffmpeg`` format by e.g ``ros2 bag record /out/ffmpeg``. 151 | 152 | #### Republishing compressed images from rosbags 153 | 154 | Let's say you have stored images as ffmpeg packets in a rosbag under the topic ``/camera/image_raw/ffmpeg``. To view them use this line: 155 | ``` 156 | ros2 run image_transport republish ffmpeg --ros-args -r in/ffmpeg:=/camera/image_raw/ffmpeg 157 | 158 | ``` 159 | This will republish the topic with full image transport support. 160 | 161 | ### Setting encoding parameters when launching camera driver 162 | 163 | The ``launch`` directory contains an example launch file ``cam.launch.py`` that demonstrates 164 | how to set encoding profile and preset for e.g. a usb camera. 165 | 166 | 167 | ### How to use a custom version of libav (aka ffmpeg) 168 | 169 | See the [``ffmpeg_encoder_decoder`` repository](https://github.com/ros-misc-utilities/ffmpeg_encoder_decoder). 170 | There you will also find instructions for hardware accelerated 171 | streaming on the NVidia Jetson. 172 | 173 | ## License 174 | 175 | This software is issued under the Apache License Version 2.0. 176 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Bernd Pfrommer 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | import os 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath('.')) 20 | 21 | project = 'ffmpeg_image_transport' 22 | # copyright = '2024, Bernd Pfrommer' 23 | author = 'Bernd Pfrommer' 24 | 25 | 26 | # Add any Sphinx extension module names here, as strings. 27 | extensions = [ 28 | 'myst_parser', 29 | 'sphinx.ext.autodoc', 30 | 'sphinx.ext.doctest', 31 | 'sphinx_rtd_theme', 32 | 'sphinx.ext.coverage', 33 | 'sphinx.ext.intersphinx', 34 | 'sphinx.ext.autosummary', 35 | 'sphinx.ext.napoleon', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | source_suffix = '.rst' 42 | # exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 43 | exclude_patterns = [] 44 | 45 | # The name of the Pygments (syntax highlighting) style to use. 46 | pygments_style = None 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | # html_theme = 'alabaster' 54 | html_theme = 'sphinx_rtd_theme' 55 | htmlhelp_basename = 'ffmpeg_image_transport_doc' 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ['_static'] 61 | 62 | # -- Extension configuration ------------------------------------------------- 63 | 64 | autoclass_content = 'both' 65 | 66 | autodoc_default_options = { 67 | 'members': True, # document members 68 | 'undoc-members': True, # also document members without documentation 69 | } 70 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ffmpeg_image_transport 2 | ====================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | .. include:: readme_include.md 8 | :parser: myst_parser.sphinx_ 9 | -------------------------------------------------------------------------------- /doc/readme_include.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | :relative-images: 3 | ``` 4 | -------------------------------------------------------------------------------- /ffmpeg_image_transport.repos: -------------------------------------------------------------------------------- 1 | repositories: 2 | src/ffmpeg_image_transport_msgs: 3 | type: git 4 | url: https://github.com/ros-misc-utilities/ffmpeg_image_transport_msgs.git 5 | version: master 6 | src/ffmpeg_encoder_decoder: 7 | type: git 8 | url: https://github.com/ros-misc-utilities/ffmpeg_encoder_decoder.git 9 | version: master 10 | -------------------------------------------------------------------------------- /ffmpeg_plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This plugin encodes frames into ffmpeg compressed packets 5 | 6 | 7 | 8 | 9 | This plugin decodes frames from ffmpeg compressed packets 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /include/ffmpeg_image_transport/ffmpeg_publisher.hpp: -------------------------------------------------------------------------------- 1 | // -*-c++-*--------------------------------------------------------------------------------------- 2 | // Copyright 2023 Bernd Pfrommer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #ifndef FFMPEG_IMAGE_TRANSPORT__FFMPEG_PUBLISHER_HPP_ 17 | #define FFMPEG_IMAGE_TRANSPORT__FFMPEG_PUBLISHER_HPP_ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace ffmpeg_image_transport 26 | { 27 | using ffmpeg_image_transport_msgs::msg::FFMPEGPacket; 28 | using FFMPEGPublisherPlugin = image_transport::SimplePublisherPlugin; 29 | using Image = sensor_msgs::msg::Image; 30 | using FFMPEGPacketConstPtr = FFMPEGPacket::ConstSharedPtr; 31 | class FFMPEGPublisher : public FFMPEGPublisherPlugin 32 | { 33 | public: 34 | using ParameterDescriptor = rcl_interfaces::msg::ParameterDescriptor; 35 | using ParameterValue = rclcpp::ParameterValue; 36 | 37 | #if defined(IMAGE_TRANSPORT_API_V1) || defined(IMAGE_TRANSPORT_API_V2) 38 | using PublisherTFn = PublishFn; 39 | #else 40 | using PublisherTFn = PublisherT; 41 | #endif 42 | 43 | struct ParameterDefinition 44 | { 45 | ParameterValue defaultValue; 46 | ParameterDescriptor descriptor; 47 | }; 48 | 49 | FFMPEGPublisher(); 50 | ~FFMPEGPublisher() override; 51 | std::string getTransportName() const override { return "ffmpeg"; } 52 | 53 | protected: 54 | #if defined(IMAGE_TRANSPORT_API_V1) || defined(IMAGE_TRANSPORT_API_V2) 55 | void advertiseImpl( 56 | rclcpp::Node * node, const std::string & base_topic, rmw_qos_profile_t custom_qos) override; 57 | #else 58 | void advertiseImpl( 59 | rclcpp::Node * node, const std::string & base_topic, rmw_qos_profile_t custom_qos, 60 | rclcpp::PublisherOptions opt) override; 61 | #endif 62 | void publish(const Image & message, const PublisherTFn & publisher) const override; 63 | 64 | private: 65 | void packetReady( 66 | const std::string & frame_id, const rclcpp::Time & stamp, const std::string & codec, 67 | uint32_t width, uint32_t height, uint64_t pts, uint8_t flags, uint8_t * data, size_t sz); 68 | 69 | rmw_qos_profile_t initialize( 70 | rclcpp::Node * node, const std::string & base_name, rmw_qos_profile_t custom_qos); 71 | void declareParameter( 72 | rclcpp::Node * node, const std::string & base_name, const ParameterDefinition & definition); 73 | // variables --------- 74 | rclcpp::Logger logger_; 75 | const PublisherTFn * publishFunction_{NULL}; 76 | ffmpeg_encoder_decoder::Encoder encoder_; 77 | uint32_t frameCounter_{0}; 78 | // ---------- configurable parameters 79 | int performanceInterval_{175}; // num frames between perf printouts 80 | bool measurePerformance_{false}; 81 | }; 82 | } // namespace ffmpeg_image_transport 83 | 84 | #endif // FFMPEG_IMAGE_TRANSPORT__FFMPEG_PUBLISHER_HPP_ 85 | -------------------------------------------------------------------------------- /include/ffmpeg_image_transport/ffmpeg_subscriber.hpp: -------------------------------------------------------------------------------- 1 | // -*-c++-*--------------------------------------------------------------------------------------- 2 | // Copyright 2023 Bernd Pfrommer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #ifndef FFMPEG_IMAGE_TRANSPORT__FFMPEG_SUBSCRIBER_HPP_ 17 | #define FFMPEG_IMAGE_TRANSPORT__FFMPEG_SUBSCRIBER_HPP_ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace ffmpeg_image_transport 25 | { 26 | using Image = sensor_msgs::msg::Image; 27 | using ImageConstPtr = Image::ConstSharedPtr; 28 | using ffmpeg_image_transport_msgs::msg::FFMPEGPacket; 29 | using FFMPEGPacketConstPtr = FFMPEGPacket::ConstSharedPtr; 30 | using FFMPEGSubscriberPlugin = image_transport::SimpleSubscriberPlugin; 31 | 32 | class FFMPEGSubscriber : public FFMPEGSubscriberPlugin 33 | { 34 | public: 35 | FFMPEGSubscriber(); 36 | ~FFMPEGSubscriber(); 37 | 38 | std::string getTransportName() const override { return "ffmpeg"; } 39 | 40 | protected: 41 | void internalCallback(const FFMPEGPacketConstPtr & msg, const Callback & user_cb) override; 42 | 43 | #ifdef IMAGE_TRANSPORT_API_V1 44 | void subscribeImpl( 45 | rclcpp::Node * node, const std::string & base_topic, const Callback & callback, 46 | rmw_qos_profile_t custom_qos) override; 47 | #else 48 | void subscribeImpl( 49 | rclcpp::Node * node, const std::string & base_topic, const Callback & callback, 50 | rmw_qos_profile_t custom_qos, rclcpp::SubscriptionOptions) override; 51 | #endif 52 | 53 | private: 54 | void frameReady(const ImageConstPtr & img, bool /*isKeyFrame*/) const; 55 | void initialize(rclcpp::Node * node, const std::string & base_topics); 56 | // -------------- variables 57 | rclcpp::Logger logger_; 58 | rclcpp::Node * node_; 59 | ffmpeg_encoder_decoder::Decoder decoder_; 60 | std::string decoderType_; 61 | const Callback * userCallback_; 62 | std::string param_namespace_; 63 | }; 64 | } // namespace ffmpeg_image_transport 65 | #endif // FFMPEG_IMAGE_TRANSPORT__FFMPEG_SUBSCRIBER_HPP_ 66 | -------------------------------------------------------------------------------- /launch/cam.launch.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright 2023 Bernd Pfrommer 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # 17 | 18 | import argparse 19 | import os 20 | import sys 21 | 22 | from ament_index_python.packages import get_package_share_directory 23 | from launch import LaunchDescription 24 | from launch_ros.actions import Node 25 | 26 | 27 | def generate_launch_description(): 28 | ld = LaunchDescription() 29 | 30 | parser = argparse.ArgumentParser(description='usb_cam launch') 31 | parser.add_argument( 32 | '-n', '--node-name', dest='node_name', type=str, help='name for device', default='usb_cam' 33 | ) 34 | 35 | args, unknown = parser.parse_known_args(sys.argv[4:]) 36 | 37 | usb_cam_dir = get_package_share_directory('usb_cam') 38 | 39 | # get path to params file 40 | params_path = os.path.join(usb_cam_dir, 'config', 'params.yaml') 41 | 42 | node_name = args.node_name 43 | 44 | print(params_path) 45 | ld.add_action( 46 | Node( 47 | package='usb_cam', 48 | executable='usb_cam_node_exe', 49 | output='screen', 50 | name=node_name, 51 | namespace='camera', 52 | parameters=[ 53 | params_path, 54 | { 55 | '.image_raw.ffmpeg.encoding': 'h264_nvenc', 56 | '.image_raw.ffmpeg.profile': 'main', 57 | '.image_raw.ffmpeg.preset': 'll', 58 | '.image_raw.ffmpeg.gop': 15, 59 | '.image_raw.ffmpeg.qmax': 31, 60 | }, 61 | ], 62 | ) 63 | ) 64 | 65 | return ld 66 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ffmpeg_image_transport 5 | 1.0.0 6 | 7 | ffmpeg_image_transport provides a plugin to image_transport for 8 | transparently sending an image stream encoded with ffmpeg. 9 | 10 | Bernd Pfrommer 11 | Apache-2 12 | 13 | http://www.ros.org/wiki/image_transport_plugins 14 | Bernd Pfrommer 15 | 16 | ament_cmake 17 | ament_cmake_ros 18 | ros_environment 19 | 20 | ffmpeg_encoder_decoder 21 | ffmpeg_image_transport_msgs 22 | image_transport 23 | pluginlib 24 | rclcpp 25 | rcutils 26 | sensor_msgs 27 | std_msgs 28 | 29 | ament_lint_common 30 | ament_lint_auto 31 | ament_cmake_clang_format 32 | 33 | 34 | rosidl_interface_packages 35 | 36 | 37 | ament_cmake 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /rosdoc2.yaml: -------------------------------------------------------------------------------- 1 | ## This 'attic section' self-documents this file's type and version. 2 | type: 'rosdoc2 config' 3 | version: 1 4 | 5 | --- 6 | 7 | settings: 8 | ## If this is true, a standard index page is generated in the output directory. 9 | ## It uses the package information from the 'package.xml' to show details 10 | ## about the package, creates a table of contents for the various builders 11 | ## that were run, and may contain links to things like build farm jobs for 12 | ## this package or links to other versions of this package. 13 | ## If this is not specified explicitly, it defaults to 'true'. 14 | generate_package_index: true 15 | 16 | ## Point to python sources relative to package.xml file 17 | python_source: 'ffmpeg_image_transport' 18 | 19 | ## Don't run doxygen 20 | always_run_doxygen: false 21 | 22 | ## Build python API docs 23 | ## This is most useful if the user would like to generate Python API 24 | ## documentation for a package that is not of the `ament_python` build type. 25 | always_run_sphinx_apidoc: false 26 | 27 | # disable breathe and exhale 28 | enable_breathe: false 29 | enable_exhale: false 30 | 31 | # This setting, if provided, will override the build_type of this package 32 | # for documentation purposes only. If not provided, documentation will be 33 | # generated assuming the build_type in package.xml. 34 | override_build_type: 'ament_python' 35 | 36 | builders: 37 | - sphinx: { 38 | name: 'ffmpeg_image_transport', 39 | ## This path is relative to output staging. 40 | sphinx_sourcedir: 'doc/', 41 | output_dir: '' 42 | } 43 | -------------------------------------------------------------------------------- /src/ffmpeg_publisher.cpp: -------------------------------------------------------------------------------- 1 | // -*-c++-*--------------------------------------------------------------------------------------- 2 | // Copyright 2023 Bernd Pfrommer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #include 17 | #include 18 | 19 | using namespace std::placeholders; 20 | 21 | namespace ffmpeg_image_transport 22 | { 23 | using ParameterDefinition = FFMPEGPublisher::ParameterDefinition; 24 | using ParameterValue = FFMPEGPublisher::ParameterValue; 25 | using ParameterDescriptor = FFMPEGPublisher::ParameterDescriptor; 26 | 27 | static const ParameterDefinition params[] = { 28 | {ParameterValue("libx264"), 29 | ParameterDescriptor() 30 | .set__name("encoding") 31 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 32 | .set__description("ffmpeg encoder to use, see ffmpeg h264 supported encoders") 33 | .set__read_only(false)}, 34 | {ParameterValue(""), ParameterDescriptor() 35 | .set__name("preset") 36 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 37 | .set__description("ffmpeg encoder preset") 38 | .set__read_only(false)}, 39 | {ParameterValue(""), ParameterDescriptor() 40 | .set__name("tune") 41 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 42 | .set__description("ffmpeg encoder tune") 43 | .set__read_only(false)}, 44 | {ParameterValue(""), ParameterDescriptor() 45 | .set__name("delay") 46 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 47 | .set__description("ffmpeg encoder delay") 48 | .set__read_only(false)}, 49 | {ParameterValue(""), ParameterDescriptor() 50 | .set__name("crf") 51 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 52 | .set__description("constant rate factor") 53 | .set__read_only(false)}, 54 | {ParameterValue(""), ParameterDescriptor() 55 | .set__name("pixel_format") 56 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 57 | .set__description("pixel format to use for encoding") 58 | .set__read_only(false)}, 59 | {ParameterValue(static_cast(10)), 60 | ParameterDescriptor() 61 | .set__name("qmax") 62 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER) 63 | .set__description("max video quantizer scale, see ffmpeg docs") 64 | .set__read_only(false) 65 | .set__integer_range( 66 | {rcl_interfaces::msg::IntegerRange().set__from_value(-1).set__to_value(1024).set__step(1)})}, 67 | {ParameterValue(static_cast(8242880)), 68 | ParameterDescriptor() 69 | .set__name("bit_rate") 70 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER) 71 | .set__description("target bit rate, see ffmpeg docs") 72 | .set__read_only(false) 73 | .set__integer_range({rcl_interfaces::msg::IntegerRange() 74 | .set__from_value(1) 75 | .set__to_value(std::numeric_limits::max()) 76 | .set__step(1)})}, 77 | {ParameterValue(static_cast(10)), 78 | ParameterDescriptor() 79 | .set__name("gop_size") 80 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER) 81 | .set__description("gop size (distance between keyframes)") 82 | .set__read_only(false) 83 | .set__integer_range({rcl_interfaces::msg::IntegerRange() 84 | .set__from_value(1) 85 | .set__to_value(std::numeric_limits::max()) 86 | .set__step(1)})}, 87 | {ParameterValue(false), ParameterDescriptor() 88 | .set__name("measure_performance") 89 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_BOOL) 90 | .set__description("enable performance timing") 91 | .set__read_only(false)}, 92 | {ParameterValue(static_cast(175)), 93 | ParameterDescriptor() 94 | .set__name("performance_interval") 95 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_INTEGER) 96 | .set__description("after how many frames to print perf info") 97 | .set__read_only(false) 98 | .set__integer_range({rcl_interfaces::msg::IntegerRange() 99 | .set__from_value(1) 100 | .set__to_value(std::numeric_limits::max()) 101 | .set__step(1)})}, 102 | }; 103 | 104 | FFMPEGPublisher::FFMPEGPublisher() : logger_(rclcpp::get_logger("FFMPEGPublisher")) {} 105 | 106 | FFMPEGPublisher::~FFMPEGPublisher() {} 107 | 108 | // This code was lifted from compressed_image_transport 109 | 110 | void FFMPEGPublisher::declareParameter( 111 | rclcpp::Node * node, const std::string & base_name, const ParameterDefinition & definition) 112 | { 113 | // transport scoped parameter (e.g. image_raw.compressed.format) 114 | const std::string transport_name = getTransportName(); 115 | const std::string param_name = 116 | base_name + "." + transport_name + "." + definition.descriptor.name; 117 | rclcpp::ParameterValue v; 118 | try { 119 | v = node->declare_parameter(param_name, definition.defaultValue, definition.descriptor); 120 | } catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException &) { 121 | RCLCPP_DEBUG(logger_, "%s was previously declared", definition.descriptor.name.c_str()); 122 | v = node->get_parameter(param_name).get_parameter_value(); 123 | } 124 | const auto & n = definition.descriptor.name; 125 | if (n == "encoding") { 126 | encoder_.setEncoder(v.get()); 127 | RCLCPP_INFO_STREAM(logger_, "using encoder: " << v.get()); 128 | } else if (n == "preset") { 129 | encoder_.setPreset(v.get()); 130 | } else if (n == "tune") { 131 | encoder_.setTune(v.get()); 132 | } else if (n == "delay") { 133 | encoder_.setDelay(v.get()); 134 | } else if (n == "crf") { 135 | encoder_.setCRF(v.get()); 136 | } else if (n == "pixel_format") { 137 | encoder_.setPixelFormat(v.get()); 138 | } else if (n == "qmax") { 139 | encoder_.setQMax(v.get()); 140 | } else if (n == "bit_rate") { 141 | encoder_.setBitRate(v.get()); 142 | } else if (n == "gop_size") { 143 | encoder_.setGOPSize(v.get()); 144 | } else if (n == "measure_performance") { 145 | measurePerformance_ = v.get(); 146 | encoder_.setMeasurePerformance(v.get()); 147 | } else if (n == "performance_interval") { 148 | performanceInterval_ = v.get(); 149 | } else { 150 | RCLCPP_ERROR_STREAM(logger_, "unknown parameter: " << n); 151 | } 152 | } 153 | 154 | void FFMPEGPublisher::packetReady( 155 | const std::string & frame_id, const rclcpp::Time & stamp, const std::string & codec, 156 | uint32_t width, uint32_t height, uint64_t pts, uint8_t flags, uint8_t * data, size_t sz) 157 | { 158 | auto msg = std::make_shared(); 159 | msg->header.frame_id = frame_id; 160 | msg->header.stamp = stamp; 161 | msg->encoding = codec; 162 | msg->width = width; 163 | msg->height = height; 164 | msg->pts = pts; 165 | msg->flags = flags; 166 | msg->data.assign(data, data + sz); 167 | #if defined(IMAGE_TRANSPORT_API_V1) || defined(IMAGE_TRANSPORT_API_V2) 168 | (*publishFunction_)(*msg); 169 | #else 170 | (*publishFunction_)->publish(*msg); 171 | #endif 172 | } 173 | 174 | #if defined(IMAGE_TRANSPORT_API_V1) || defined(IMAGE_TRANSPORT_API_V2) 175 | void FFMPEGPublisher::advertiseImpl( 176 | rclcpp::Node * node, const std::string & base_topic, rmw_qos_profile_t custom_qos) 177 | { 178 | auto qos = initialize(node, base_topic, custom_qos); 179 | FFMPEGPublisherPlugin::advertiseImpl(node, base_topic, qos); 180 | } 181 | #else 182 | void FFMPEGPublisher::advertiseImpl( 183 | rclcpp::Node * node, const std::string & base_topic, rmw_qos_profile_t custom_qos, 184 | rclcpp::PublisherOptions opt) 185 | { 186 | auto qos = initialize(node, base_topic, custom_qos); 187 | FFMPEGPublisherPlugin::advertiseImpl(node, base_topic, qos, opt); 188 | } 189 | #endif 190 | 191 | rmw_qos_profile_t FFMPEGPublisher::initialize( 192 | rclcpp::Node * node, const std::string & base_topic, rmw_qos_profile_t custom_qos) 193 | { 194 | // namespace handling code lifted from compressed_image_transport 195 | const uint ns_len = node->get_effective_namespace().length(); 196 | std::string param_base_name = base_topic.substr(ns_len); 197 | std::replace(param_base_name.begin(), param_base_name.end(), '/', '.'); 198 | 199 | for (const auto & p : params) { 200 | declareParameter(node, param_base_name, p); 201 | } 202 | // bump queue size to 2 * distance between keyframes 203 | custom_qos.depth = std::max(static_cast(custom_qos.depth), 2 * encoder_.getGOPSize()); 204 | return (custom_qos); 205 | } 206 | 207 | void FFMPEGPublisher::publish(const Image & msg, const PublisherTFn & publisher) const 208 | { 209 | FFMPEGPublisher * me = const_cast(this); 210 | me->publishFunction_ = &publisher; 211 | if (!me->encoder_.isInitialized()) { 212 | if (!me->encoder_.initialize( 213 | msg.width, msg.height, 214 | std::bind(&FFMPEGPublisher::packetReady, me, _1, _2, _3, _4, _5, _6, _7, _8, _9))) { 215 | RCLCPP_ERROR_STREAM(logger_, "cannot initialize encoder!"); 216 | return; 217 | } 218 | } 219 | // may trigger packetReady() callback(s) from encoder! 220 | me->encoder_.encodeImage(msg); 221 | 222 | if (measurePerformance_) { 223 | if (static_cast(++me->frameCounter_) > performanceInterval_) { 224 | me->encoder_.printTimers(logger_.get_name()); 225 | me->encoder_.resetTimers(); 226 | me->frameCounter_ = 0; 227 | } 228 | } 229 | } 230 | 231 | } // namespace ffmpeg_image_transport 232 | -------------------------------------------------------------------------------- /src/ffmpeg_subscriber.cpp: -------------------------------------------------------------------------------- 1 | // -*-c++-*--------------------------------------------------------------------------------------- 2 | // Copyright 2023 Bernd Pfrommer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using namespace std::placeholders; 23 | 24 | namespace ffmpeg_image_transport 25 | { 26 | FFMPEGSubscriber::FFMPEGSubscriber() : logger_(rclcpp::get_logger("FFMPEGSubscriber")) {} 27 | 28 | FFMPEGSubscriber::~FFMPEGSubscriber() {} 29 | 30 | void FFMPEGSubscriber::frameReady(const ImageConstPtr & img, bool) const { (*userCallback_)(img); } 31 | 32 | #ifdef IMAGE_TRANSPORT_API_V1 33 | void FFMPEGSubscriber::subscribeImpl( 34 | rclcpp::Node * node, const std::string & base_topic, const Callback & callback, 35 | rmw_qos_profile_t custom_qos) 36 | { 37 | initialize(node, base_topic); 38 | FFMPEGSubscriberPlugin::subscribeImpl(node, base_topic, callback, custom_qos); 39 | } 40 | #else 41 | void FFMPEGSubscriber::subscribeImpl( 42 | rclcpp::Node * node, const std::string & base_topic, const Callback & callback, 43 | rmw_qos_profile_t custom_qos, rclcpp::SubscriptionOptions opt) 44 | { 45 | initialize(node, base_topic); 46 | #ifdef IMAGE_TRANSPORT_API_V2 47 | (void)opt; // to suppress compiler warning 48 | FFMPEGSubscriberPlugin::subscribeImpl(node, base_topic, callback, custom_qos); 49 | #else 50 | FFMPEGSubscriberPlugin::subscribeImpl(node, base_topic, callback, custom_qos, opt); 51 | #endif 52 | } 53 | #endif 54 | 55 | void FFMPEGSubscriber::initialize(rclcpp::Node * node, const std::string & base_topic) 56 | { 57 | node_ = node; 58 | // Declare Parameters 59 | uint ns_len = node->get_effective_namespace().length(); 60 | std::string param_base_name = base_topic.substr(ns_len); 61 | std::replace(param_base_name.begin(), param_base_name.end(), '/', '.'); 62 | param_namespace_ = param_base_name + "." + getTransportName() + ".map."; 63 | // create parameters from default map 64 | for (const auto & kv : ffmpeg_encoder_decoder::Decoder::getDefaultEncoderToDecoderMap()) { 65 | const std::string key = param_namespace_ + kv.first; 66 | rclcpp::ParameterValue v; 67 | try { 68 | rcl_interfaces::msg::ParameterDescriptor pd; 69 | pd.set__name(kv.first) 70 | .set__type(rcl_interfaces::msg::ParameterType::PARAMETER_STRING) 71 | .set__description("ffmpeg decoder map entry") 72 | .set__read_only(false); 73 | v = node->declare_parameter(key, rclcpp::ParameterValue(kv.second), pd); 74 | } catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException &) { 75 | RCLCPP_DEBUG_STREAM(logger_, "was previously declared: " << kv.first); 76 | v = node->get_parameter(key).get_parameter_value(); 77 | } 78 | if (v.get() != kv.second) { 79 | RCLCPP_INFO_STREAM( 80 | logger_, "overriding default decoder " << kv.second << " for " << kv.first << " with " 81 | << v.get()); 82 | } 83 | } 84 | const bool mp = ffmpeg_encoder_decoder::get_safe_param( 85 | node_, param_namespace_ + "measure_performance", false); 86 | decoder_.setMeasurePerformance(mp); 87 | } 88 | 89 | void FFMPEGSubscriber::internalCallback(const FFMPEGPacketConstPtr & msg, const Callback & user_cb) 90 | { 91 | if (!decoder_.isInitialized()) { 92 | if (msg->flags == 0) { 93 | return; // wait for key frame! 94 | } 95 | if (msg->encoding.empty()) { 96 | RCLCPP_ERROR_STREAM(logger_, "no encoding provided!"); 97 | return; 98 | } 99 | userCallback_ = &user_cb; 100 | const std::string decoder = ffmpeg_encoder_decoder::get_safe_param( 101 | node_, param_namespace_ + msg->encoding, ""); 102 | if (decoder.empty()) { 103 | RCLCPP_ERROR_STREAM(logger_, "no valid decoder found for encoding: " << msg->encoding); 104 | return; 105 | } 106 | if (!decoder_.initialize( 107 | msg->encoding, std::bind(&FFMPEGSubscriber::frameReady, this, _1, _2), decoder)) { 108 | RCLCPP_ERROR_STREAM(logger_, "cannot initialize decoder!"); 109 | return; 110 | } 111 | } 112 | decoder_.decodePacket( 113 | msg->encoding, &msg->data[0], msg->data.size(), msg->pts, msg->header.frame_id, 114 | msg->header.stamp); 115 | } 116 | } // namespace ffmpeg_image_transport 117 | -------------------------------------------------------------------------------- /src/manifest.cpp: -------------------------------------------------------------------------------- 1 | // -*-c++-*--------------------------------------------------------------------------------------- 2 | // Copyright 2023 Bernd Pfrommer 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #include 17 | 18 | #include "ffmpeg_image_transport/ffmpeg_publisher.hpp" 19 | #include "ffmpeg_image_transport/ffmpeg_subscriber.hpp" 20 | 21 | PLUGINLIB_EXPORT_CLASS(ffmpeg_image_transport::FFMPEGPublisher, image_transport::PublisherPlugin) 22 | PLUGINLIB_EXPORT_CLASS(ffmpeg_image_transport::FFMPEGSubscriber, image_transport::SubscriberPlugin) 23 | --------------------------------------------------------------------------------