├── nodelet_plugins.xml ├── launch └── rtsp_streams.launch ├── config └── stream_setup.yaml ├── .gitignore ├── include └── image2rtsp.h ├── LICENSE ├── package.xml ├── README.md ├── src ├── video.cpp └── image2rtsp.cpp └── CMakeLists.txt /nodelet_plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nodelet to transfert the /camera/image topic from openni2_camera over rtsp. 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /launch/rtsp_streams.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/stream_setup.yaml: -------------------------------------------------------------------------------- 1 | # Set up your streams to rtsp here. 2 | port: "8554" 3 | streams: # Cannot rename - must leave this as is. 4 | 5 | stream-x: 6 | type: cam 7 | source: "v4l2src device=/dev/video0 ! videoconvert ! videoscale ! video/x-raw,framerate=15/1,width=1280,height=720" 8 | mountpoint: /front 9 | bitrate: 500 10 | 11 | stream-yay: 12 | type: topic 13 | source: /usb_cam1/image_raw 14 | mountpoint: /back 15 | caps: video/x-raw,framerate=10/1,width=640,height=480 16 | bitrate: 500 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | devel/ 2 | logs/ 3 | build/ 4 | bin/ 5 | lib/ 6 | msg_gen/ 7 | srv_gen/ 8 | msg/*Action.msg 9 | msg/*ActionFeedback.msg 10 | msg/*ActionGoal.msg 11 | msg/*ActionResult.msg 12 | msg/*Feedback.msg 13 | msg/*Goal.msg 14 | msg/*Result.msg 15 | msg/_*.py 16 | build_isolated/ 17 | devel_isolated/ 18 | 19 | # Generated by dynamic reconfigure 20 | *.cfgc 21 | /cfg/cpp/ 22 | /cfg/*.py 23 | 24 | # Ignore generated docs 25 | *.dox 26 | *.wikidoc 27 | 28 | # eclipse stuff 29 | .project 30 | .cproject 31 | 32 | # qcreator stuff 33 | CMakeLists.txt.user 34 | 35 | srv/_*.py 36 | *.pcd 37 | *.pyc 38 | qtcreator-* 39 | *.user 40 | 41 | /planning/cfg 42 | /planning/docs 43 | /planning/src 44 | 45 | *~ 46 | 47 | # Emacs 48 | .#* 49 | 50 | # Catkin custom files 51 | CATKIN_IGNORE 52 | -------------------------------------------------------------------------------- /include/image2rtsp.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGE_TO_RTSP_H 2 | #define IMAGE_TO_RTSP_H 3 | 4 | namespace image2rtsp { 5 | class Image2RTSPNodelet : public nodelet::Nodelet { 6 | public: 7 | GstRTSPServer *rtsp_server; 8 | void onInit(); 9 | void url_connected(std::string url); 10 | void url_disconnected(std::string url); 11 | void print_info(char *s); 12 | void print_error(char *s); 13 | 14 | private: 15 | std::string port; 16 | std::map subs; 17 | std::map appsrc; 18 | std::map num_of_clients; 19 | GstCaps* gst_caps_new_from_image(const sensor_msgs::Image::ConstPtr &msg); 20 | void imageCallback(const sensor_msgs::Image::ConstPtr& msg, const std::string& topic); 21 | void video_mainloop_start(); 22 | void rtsp_server_add_url(const char *url, const char *sPipeline, GstElement **appsrc); 23 | GstRTSPServer *rtsp_server_create(const std::string& port); 24 | }; 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ros_rtsp 4 | 0.0.0 5 | The ros_rtsp package 6 | 7 | 8 | 9 | 10 | samar 11 | 12 | 13 | 14 | 15 | 16 | TODO 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | catkin 52 | roscpp 53 | sensor_msgs 54 | nodelet 55 | roscpp 56 | sensor_msgs 57 | roscpp 58 | sensor_msgs 59 | nodelet 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ros_rtsp 2 | ROS package to subscribe to an ROS Image topic (and as many other video sources as you want) and serve it up as a RTSP video feed with different mount points. 3 | Should provide a real-time video feed (or as close as possible). 4 | 5 | This is still very much a work in progress. Developing on Ubuntu 16.04 and 18.04 with ROS kinetic and melodic. 6 | 7 | 8 | ## Dependencies 9 | - ROS 10 | 11 | - gstreamer libs: 12 | ```bash 13 | sudo apt-get install libgstreamer-plugins-base1.0-dev libgstreamer-plugins-good1.0-dev libgstreamer-plugins-bad1.0-dev libgstrtspserver-1.0-dev gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad 14 | ``` 15 | 16 | ## Build into your catkin workspace 17 | Navigate to your catkin workspace `src` folder. I.e. `cd ~/catkin_ws/src`. 18 | Clone this package to the repository: 19 | ```bash 20 | git clone https://github.com/CircusMonkey/ros_rtsp.git 21 | ``` 22 | 23 | Navigate back to the catkin workspace root and make the package: 24 | ```bash 25 | cd .. 26 | catkin_make pkg:=ros_rtsp 27 | ``` 28 | 29 | ## Stream Setup 30 | Change the `config/stream_setup.yaml` to suit your required streams. 31 | 32 | ```yaml 33 | # Set up your streams for rtsp here. 34 | streams: 35 | 36 | # Example v4l2 camera stream 37 | stream-1: # Can name this whatever you choose 38 | type: cam # cam - Will not look in ROS for a image. The video src is set in the 'source' parameter. 39 | source: "v4l2src device=/dev/video0 ! videoconvert ! videoscale ! video/x-raw,framerate=15/1,width=1280,height=720" # You can enter any valid gstreamer source and caps here as long as it ends in raw video 40 | mountpoint: /front # Choose the mountpoint for the rtsp stream. This will be able to be accessed from rtsp:///front 41 | bitrate: 800 # bitrate for the h264 encoding. 42 | 43 | # Example ROS Image topic stream 44 | stream2: 45 | type: topic # topic - Image is sourced from a sensor_msgs::Image topic 46 | source: /usb_cam0/image_raw # The ROS topic to subscribe to 47 | mountpoint: /back # Choose the mountpoint for the rtsp stream. This will be able to be accessed from rtsp:///back 48 | caps: video/x-raw,framerate=10/1,width=640,height=480 # Set the caps to be applied after getting the ROS Image and before the x265 encoder. 49 | bitrate: 500 50 | ``` 51 | Add as many streams as you require. 52 | 53 | ## Checking the streams 54 | Launch the streams from the ROS launch file: 55 | ```bash 56 | roslaunch ros_rtsp rtsp_streams.launch 57 | ``` 58 | 59 | In the following examples, replace the `rtsp://127.0.0.1:8554/front` with your servers IP address and mount point `rtsp://YOUR_IP:8554/MOUNT_POINT`. 60 | 61 | ### gstreamer 62 | Use `gst-launch-1.0`. You will need to install gstreamer for your client system. See https://gstreamer.freedesktop.org/documentation/installing/index.html 63 | ```bash 64 | gst-launch-1.0 -v rtspsrc location=rtsp://127.0.0.1:8554/front drop-on-latency=true use-pipeline-clock=true do-retransmission=false latency=0 protocols=GST_RTSP_LOWER_TRANS_UDP ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink sync=true 65 | ``` 66 | 67 | ### mpv 68 | ```bash 69 | mpv --no-cache --untimed --no-demuxer-thread --vd-lavc-threads=1 rtsp://127.0.0.1:8554/front 70 | ``` 71 | 72 | ### VLC 73 | VLC adds way too much latency. Please don't use it for this purpose. If you want to try, this is the command that was the least slow (Let me know if you find a better command): 74 | ```bash 75 | cvlc --no-audio --mux none --demux none --deinterlace 0 --no-autoscale --avcodec-hw=any --no-auto-preparse --sout-rtp-proto=udp --network-caching=300 --realrtsp-caching=0 --sout-udp-caching=0 --clock-jitter=0 --rtp-max-misorder=0 rtsp://127.0.0.1:8554/front :udp-timeout=0 76 | ``` 77 | If you wish to use the VLC mobile app to stream on Android or iOS, navigate to the network or streams menu and type in your server URL and mountpoint e.g. `rtsp://127.0.0.1:8554/front` 78 | 79 | 80 | ## Debugging 81 | - If too much latency is encounted with multiple streams running, the server computer may be maxing out its processor trying to encode all the streams. Try reducing the resolution of the source caps. 82 | - The ROS Image topic stream may be buggy with framerates too fast for the Image publisher and the buffer writing. Stick with 10/1 fps unless you want to debug? :) 83 | - If too many frames are being dropped, it is likely due to network bandwidth. Try dropping the bitrate. 84 | - If the ROS topic isn't available, you will get a `can't prepare media` error after a delay. 85 | -------------------------------------------------------------------------------- /src/video.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "sensor_msgs/Image.h" 12 | #include 13 | 14 | using namespace std; 15 | using namespace image2rtsp; 16 | 17 | 18 | static void *mainloop(void *arg) { 19 | GMainLoop *loop = g_main_loop_new(NULL, FALSE); 20 | 21 | g_main_loop_run(loop); 22 | 23 | g_main_destroy(loop); 24 | return NULL; 25 | } 26 | 27 | 28 | void Image2RTSPNodelet::video_mainloop_start() { 29 | pthread_t tloop; 30 | 31 | gst_init(NULL, NULL); 32 | pthread_create(&tloop, NULL, &mainloop, NULL); 33 | } 34 | 35 | 36 | static void client_options(GstRTSPClient *client, GstRTSPContext *state, Image2RTSPNodelet *nodelet) { 37 | if (state->uri) { 38 | nodelet->url_connected(state->uri->abspath); 39 | } 40 | } 41 | 42 | 43 | static void client_teardown(GstRTSPClient *client, GstRTSPContext *state, Image2RTSPNodelet *nodelet) { 44 | if (state->uri) { 45 | nodelet->url_disconnected(state->uri->abspath); 46 | } 47 | } 48 | 49 | 50 | static void new_client(GstRTSPServer *server, GstRTSPClient *client, Image2RTSPNodelet *nodelet) { 51 | nodelet->print_info((char *)"New RTSP client"); 52 | g_signal_connect(client, "options-request", G_CALLBACK(client_options), nodelet); 53 | g_signal_connect(client, "teardown-request", G_CALLBACK(client_teardown), nodelet); 54 | } 55 | 56 | /* this function is periodically run to clean up the expired sessions from the pool. */ 57 | static gboolean session_cleanup(Image2RTSPNodelet *nodelet, gboolean ignored) 58 | { 59 | GstRTSPServer *server = nodelet->rtsp_server; 60 | GstRTSPSessionPool *pool; 61 | int num; 62 | 63 | pool = gst_rtsp_server_get_session_pool(server); 64 | num = gst_rtsp_session_pool_cleanup(pool); 65 | g_object_unref(pool); 66 | 67 | if (num > 0) { 68 | char s[32]; 69 | snprintf(s, 32, (char *)"Sessions cleaned: %d", num); 70 | nodelet->print_info(s); 71 | } 72 | 73 | return TRUE; 74 | } 75 | 76 | GstRTSPServer *Image2RTSPNodelet::rtsp_server_create(const std::string& port) { 77 | GstRTSPServer *server; 78 | 79 | /* create a server instance */ 80 | server = gst_rtsp_server_new(); 81 | 82 | // char *port = (char *) port; 83 | g_object_set(server, "service", port.c_str(), NULL); 84 | 85 | /* attach the server to the default maincontext */ 86 | gst_rtsp_server_attach(server, NULL); 87 | 88 | g_signal_connect(server, "client-connected", G_CALLBACK(new_client), this); 89 | 90 | /* add a timeout for the session cleanup */ 91 | g_timeout_add_seconds(2, (GSourceFunc)session_cleanup, this); 92 | 93 | return server; 94 | } 95 | 96 | 97 | /* called when a new media pipeline is constructed. We can query the 98 | * pipeline and configure our appsrc */ 99 | static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, GstElement **appsrc) 100 | { if (appsrc) { 101 | GstElement *pipeline = gst_rtsp_media_get_element(media); 102 | 103 | *appsrc = gst_bin_get_by_name(GST_BIN(pipeline), "imagesrc"); 104 | 105 | /* this instructs appsrc that we will be dealing with timed buffer */ 106 | gst_util_set_object_arg(G_OBJECT(*appsrc), "format", "time"); 107 | 108 | gst_object_unref(pipeline); 109 | } 110 | else 111 | { 112 | guint i, n_streams; 113 | n_streams = gst_rtsp_media_n_streams (media); 114 | 115 | for (i = 0; i < n_streams; i++) { 116 | GstRTSPAddressPool *pool; 117 | GstRTSPStream *stream; 118 | gchar *min, *max; 119 | 120 | stream = gst_rtsp_media_get_stream (media, i); 121 | 122 | /* make a new address pool */ 123 | pool = gst_rtsp_address_pool_new (); 124 | 125 | min = g_strdup_printf ("224.3.0.%d", (2 * i) + 1); 126 | max = g_strdup_printf ("224.3.0.%d", (2 * i) + 2); 127 | gst_rtsp_address_pool_add_range (pool, min, max, 128 | 5000 + (10 * i), 5010 + (10 * i), 1); 129 | g_free (min); 130 | g_free (max); 131 | 132 | gst_rtsp_stream_set_address_pool (stream, pool); 133 | g_object_unref (pool); 134 | } 135 | } 136 | } 137 | 138 | 139 | void Image2RTSPNodelet::rtsp_server_add_url(const char *url, const char *sPipeline, GstElement **appsrc) { 140 | GstRTSPMountPoints *mounts; 141 | GstRTSPMediaFactory *factory; 142 | 143 | /* get the mount points for this server, every server has a default object 144 | * that be used to map uri mount points to media factories */ 145 | mounts = gst_rtsp_server_get_mount_points(rtsp_server); 146 | 147 | /* make a media factory for a test stream. The default media factory can use 148 | * gst-launch syntax to create pipelines. 149 | * any launch line works as long as it contains elements named pay%d. Each 150 | * element with pay%d names will be a stream */ 151 | factory = gst_rtsp_media_factory_new(); 152 | gst_rtsp_media_factory_set_launch(factory, sPipeline); 153 | 154 | /* notify when our media is ready, This is called whenever someone asks for 155 | * the media and a new pipeline is created */ 156 | g_signal_connect(factory, "media-configure", (GCallback)media_configure, appsrc); 157 | 158 | gst_rtsp_media_factory_set_shared(factory, TRUE); 159 | 160 | /* attach the factory to the url */ 161 | gst_rtsp_mount_points_add_factory(mounts, url, factory); 162 | 163 | /* don't need the ref to the mounts anymore */ 164 | g_object_unref(mounts); 165 | } 166 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(ros_rtsp) 3 | 4 | ## Find catkin macros and libraries 5 | ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) 6 | ## is used, also find other catkin packages 7 | find_package(OpenCV REQUIRED) 8 | find_package(PkgConfig REQUIRED) 9 | pkg_check_modules(GST REQUIRED 10 | gstreamer-1.0 11 | gstreamer-app-1.0 12 | gstreamer-rtsp-server-1.0 13 | ) 14 | 15 | 16 | find_package(catkin REQUIRED COMPONENTS 17 | roscpp 18 | sensor_msgs 19 | nodelet 20 | ) 21 | 22 | ## System dependencies are found with CMake's conventions 23 | # find_package(Boost REQUIRED COMPONENTS system) 24 | 25 | 26 | ## Uncomment this if the package has a setup.py. This macro ensures 27 | ## modules and global scripts declared therein get installed 28 | ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html 29 | # catkin_python_setup() 30 | 31 | ################################################ 32 | ## Declare ROS messages, services and actions ## 33 | ################################################ 34 | 35 | ## To declare and build messages, services or actions from within this 36 | ## package, follow these steps: 37 | ## * Let MSG_DEP_SET be the set of packages whose message types you use in 38 | ## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). 39 | ## * In the file package.xml: 40 | ## * add a build_depend tag for "message_generation" 41 | ## * add a build_depend and a exec_depend tag for each package in MSG_DEP_SET 42 | ## * If MSG_DEP_SET isn't empty the following dependency has been pulled in 43 | ## but can be declared for certainty nonetheless: 44 | ## * add a exec_depend tag for "message_runtime" 45 | ## * In this file (CMakeLists.txt): 46 | ## * add "message_generation" and every package in MSG_DEP_SET to 47 | ## find_package(catkin REQUIRED COMPONENTS ...) 48 | ## * add "message_runtime" and every package in MSG_DEP_SET to 49 | ## catkin_package(CATKIN_DEPENDS ...) 50 | ## * uncomment the add_*_files sections below as needed 51 | ## and list every .msg/.srv/.action file to be processed 52 | ## * uncomment the generate_messages entry below 53 | ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) 54 | 55 | ## Generate messages in the 'msg' folder 56 | # add_message_files( 57 | # FILES 58 | # Message1.msg 59 | # Message2.msg 60 | # ) 61 | 62 | ## Generate services in the 'srv' folder 63 | # add_service_files( 64 | # FILES 65 | # Service1.srv 66 | # Service2.srv 67 | # ) 68 | 69 | ## Generate actions in the 'action' folder 70 | # add_action_files( 71 | # FILES 72 | # Action1.action 73 | # Action2.action 74 | # ) 75 | 76 | ## Generate added messages and services with any dependencies listed here 77 | # generate_messages( 78 | # DEPENDENCIES 79 | # sensor_msgs# std_msgs 80 | # ) 81 | 82 | ################################################ 83 | ## Declare ROS dynamic reconfigure parameters ## 84 | ################################################ 85 | 86 | ## To declare and build dynamic reconfigure parameters within this 87 | ## package, follow these steps: 88 | ## * In the file package.xml: 89 | ## * add a build_depend and a exec_depend tag for "dynamic_reconfigure" 90 | ## * In this file (CMakeLists.txt): 91 | ## * add "dynamic_reconfigure" to 92 | ## find_package(catkin REQUIRED COMPONENTS ...) 93 | ## * uncomment the "generate_dynamic_reconfigure_options" section below 94 | ## and list every .cfg file to be processed 95 | 96 | ## Generate dynamic reconfigure parameters in the 'cfg' folder 97 | # generate_dynamic_reconfigure_options( 98 | # cfg/DynReconf1.cfg 99 | # cfg/DynReconf2.cfg 100 | # ) 101 | 102 | ################################### 103 | ## catkin specific configuration ## 104 | ################################### 105 | ## The catkin_package macro generates cmake config files for your package 106 | ## Declare things to be passed to dependent projects 107 | ## INCLUDE_DIRS: uncomment this if your package contains header files 108 | ## LIBRARIES: libraries you create in this project that dependent projects also need 109 | ## CATKIN_DEPENDS: catkin_packages dependent projects also need 110 | ## DEPENDS: system dependencies of this project that dependent projects also need 111 | catkin_package( 112 | # INCLUDE_DIRS include 113 | # LIBRARIES ros_rtsp 114 | CATKIN_DEPENDS roscpp sensor_msgs 115 | # DEPENDS system_lib 116 | ) 117 | 118 | ########### 119 | ## Build ## 120 | ########### 121 | 122 | ## Specify additional locations of header files 123 | ## Your package locations should be listed before other locations 124 | include_directories( 125 | include 126 | ${catkin_INCLUDE_DIRS} 127 | ${GST_INCLUDE_DIRS} 128 | ) 129 | 130 | ## Declare a C++ library 131 | # add_library(${PROJECT_NAME} 132 | # src/${PROJECT_NAME}/ros_rtsp.cpp 133 | # ) 134 | add_library(image_to_rtsp_nodelet src/image2rtsp.cpp src/video.cpp) 135 | 136 | 137 | ## Add cmake target dependencies of the library 138 | ## as an example, code may need to be generated before libraries 139 | ## either from message generation or dynamic reconfigure 140 | # add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 141 | 142 | ## Declare a C++ executable 143 | ## With catkin_make all packages are built within a single CMake context 144 | ## The recommended prefix ensures that target names across packages don't collide 145 | 146 | ## Rename C++ executable without prefix 147 | ## The above recommended prefix causes long target names, the following renames the 148 | ## target back to the shorter version for ease of user use 149 | ## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" 150 | # set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") 151 | 152 | ## Add cmake target dependencies of the executable 153 | ## same as for the library above 154 | 155 | ## Specify libraries to link a library or executable target against 156 | target_link_libraries(image_to_rtsp_nodelet 157 | ${catkin_LIBRARIES} 158 | ${GST_LIBRARIES} 159 | ) 160 | 161 | 162 | ############# 163 | ## Install ## 164 | ############# 165 | 166 | # all install targets should use catkin DESTINATION variables 167 | # See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html 168 | 169 | install(TARGETS image_to_rtsp_nodelet 170 | LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} 171 | ) 172 | 173 | install(DIRECTORY include/ 174 | DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} 175 | FILES_MATCHING PATTERN "*.h" 176 | ) 177 | 178 | install(DIRECTORY launch config 179 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 180 | ) 181 | 182 | install(FILES nodelet_plugins.xml 183 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 184 | ) 185 | 186 | ############# 187 | ## Testing ## 188 | ############# 189 | 190 | ## Add gtest based cpp test target and link libraries 191 | # catkin_add_gtest(${PROJECT_NAME}-test test/test_ros_rtsp.cpp) 192 | # if(TARGET ${PROJECT_NAME}-test) 193 | # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) 194 | # endif() 195 | 196 | ## Add folders to be run by python nosetests 197 | # catkin_add_nosetests(test) 198 | -------------------------------------------------------------------------------- /src/image2rtsp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "sensor_msgs/Image.h" 10 | #include 11 | #include 12 | 13 | 14 | using namespace std; 15 | using namespace image2rtsp; 16 | 17 | 18 | void Image2RTSPNodelet::onInit() { 19 | string pipeline, mountpoint, bitrate, caps; 20 | string pipeline_tail = " key-int-max=30 ! video/x-h264, profile=baseline ! rtph264pay name=pay0 pt=96 )"; // Gets completed based on rosparams below 21 | 22 | NODELET_DEBUG("Initializing image2rtsp nodelet..."); 23 | 24 | if (getenv((char*)"GST_DEBUG") == NULL) { 25 | // set GST_DEBUG to warning if unset 26 | putenv((char*)"GST_DEBUG=*:1"); 27 | } 28 | 29 | ros::NodeHandle& nh = getPrivateNodeHandle(); 30 | 31 | // Get the parameters from the rosparam server 32 | XmlRpc::XmlRpcValue streams; 33 | nh.getParam("streams", streams); 34 | ROS_ASSERT(streams.getType() == XmlRpc::XmlRpcValue::TypeStruct); 35 | ROS_DEBUG("Number of RTSP streams: %d", streams.size()); 36 | nh.getParam("port", this->port); 37 | 38 | video_mainloop_start(); 39 | rtsp_server = rtsp_server_create(port); 40 | 41 | // Go through and parse each stream 42 | for(XmlRpc::XmlRpcValue::ValueStruct::const_iterator it = streams.begin(); it != streams.end(); ++it) 43 | { 44 | XmlRpc::XmlRpcValue stream = streams[it->first]; 45 | ROS_DEBUG_STREAM("Found stream: " << (std::string)(it->first) << " ==> " << stream); 46 | 47 | // Convert to string for ease of use 48 | mountpoint = static_cast(stream["mountpoint"]); 49 | bitrate = std::to_string(static_cast(stream["bitrate"])); 50 | 51 | // uvc camera? 52 | if (stream["type"]=="cam") 53 | { 54 | pipeline = "( " + static_cast(stream["source"]) + " ! x264enc tune=zerolatency bitrate=" + bitrate + pipeline_tail; 55 | 56 | rtsp_server_add_url(mountpoint.c_str(), pipeline.c_str(), NULL); 57 | } 58 | // ROS Image topic? 59 | else if (stream["type"]=="topic") 60 | { 61 | /* Keep record of number of clients connected to each topic. 62 | * so we know to stop subscribing when no-one is connected. */ 63 | num_of_clients[mountpoint] = 0; 64 | appsrc[mountpoint] = NULL; 65 | caps = static_cast(stream["caps"]); 66 | 67 | // Setup the full pipeline 68 | pipeline = "( appsrc name=imagesrc do-timestamp=true min-latency=0 max-latency=0 max-bytes=1000 is-live=true ! videoconvert ! videoscale ! " + caps + " ! x264enc tune=zerolatency bitrate=" + bitrate + pipeline_tail; 69 | 70 | // Add the pipeline to the rtsp server 71 | rtsp_server_add_url(mountpoint.c_str(), pipeline.c_str(), (GstElement **)&(appsrc[mountpoint])); 72 | } 73 | else 74 | { 75 | ROS_ERROR("Undefined stream type. Check your stream_setup.yaml file."); 76 | } 77 | NODELET_INFO("Stream available at rtsp://%s:%s%s", gst_rtsp_server_get_address(rtsp_server), port.c_str(), mountpoint.c_str()); 78 | } 79 | } 80 | 81 | /* Modified from https://github.com/ProjectArtemis/gst_video_server/blob/master/src/server_nodelet.cpp */ 82 | GstCaps* Image2RTSPNodelet::gst_caps_new_from_image(const sensor_msgs::Image::ConstPtr &msg) 83 | { 84 | // http://gstreamer.freedesktop.org/data/doc/gstreamer/head/pwg/html/section-types-definitions.html 85 | static const ros::M_string known_formats = {{ 86 | {sensor_msgs::image_encodings::RGB8, "RGB"}, 87 | {sensor_msgs::image_encodings::RGB16, "RGB16"}, 88 | {sensor_msgs::image_encodings::RGBA8, "RGBA"}, 89 | {sensor_msgs::image_encodings::RGBA16, "RGBA16"}, 90 | {sensor_msgs::image_encodings::BGR8, "BGR"}, 91 | {sensor_msgs::image_encodings::BGR16, "BGR16"}, 92 | {sensor_msgs::image_encodings::BGRA8, "BGRA"}, 93 | {sensor_msgs::image_encodings::BGRA16, "BGRA16"}, 94 | {sensor_msgs::image_encodings::MONO8, "GRAY8"}, 95 | {sensor_msgs::image_encodings::MONO16, "GRAY16_LE"}, 96 | }}; 97 | 98 | if (msg->is_bigendian) { 99 | ROS_ERROR("GST: big endian image format is not supported"); 100 | return nullptr; 101 | } 102 | 103 | auto format = known_formats.find(msg->encoding); 104 | if (format == known_formats.end()) { 105 | ROS_ERROR("GST: image format '%s' unknown", msg->encoding.c_str()); 106 | return nullptr; 107 | } 108 | 109 | return gst_caps_new_simple("video/x-raw", 110 | "format", G_TYPE_STRING, format->second.c_str(), 111 | "width", G_TYPE_INT, msg->width, 112 | "height", G_TYPE_INT, msg->height, 113 | "framerate", GST_TYPE_FRACTION, 10, 1, 114 | nullptr); 115 | } 116 | 117 | 118 | void Image2RTSPNodelet::imageCallback(const sensor_msgs::Image::ConstPtr& msg, const std::string& topic) { 119 | GstBuffer *buf; 120 | 121 | GstCaps *caps; 122 | char *gst_type, *gst_format=(char *)""; 123 | // g_print("Image encoding: %s\n", msg->encoding.c_str()); 124 | if (appsrc[topic] != NULL) { 125 | // Set caps from message 126 | caps = gst_caps_new_from_image(msg); 127 | gst_app_src_set_caps(appsrc[topic], caps); 128 | 129 | buf = gst_buffer_new_allocate(nullptr, msg->data.size(), nullptr); 130 | gst_buffer_fill(buf, 0, msg->data.data(), msg->data.size()); 131 | GST_BUFFER_FLAG_SET(buf, GST_BUFFER_FLAG_LIVE); 132 | 133 | gst_app_src_push_buffer(appsrc[topic], buf); 134 | } 135 | } 136 | 137 | 138 | void Image2RTSPNodelet::url_connected(string url) { 139 | std::string mountpoint, source, type; 140 | 141 | NODELET_INFO("Client connected: %s", url.c_str()); 142 | ros::NodeHandle& nh = getPrivateNodeHandle(); 143 | 144 | // Get the parameters from the rosparam server 145 | XmlRpc::XmlRpcValue streams; 146 | nh.getParam("streams", streams); 147 | ROS_ASSERT(streams.getType() == XmlRpc::XmlRpcValue::TypeStruct); 148 | 149 | // Go through and parse each stream 150 | for(XmlRpc::XmlRpcValue::ValueStruct::const_iterator it = streams.begin(); it != streams.end(); ++it) 151 | { 152 | XmlRpc::XmlRpcValue stream = streams[it->first]; 153 | type = static_cast(stream["type"]); 154 | mountpoint = static_cast(stream["mountpoint"]); 155 | source = static_cast(stream["source"]); 156 | 157 | // Check which stream the client has connected to 158 | if (type=="topic" && url==mountpoint) { 159 | 160 | if (num_of_clients[url]==0) { 161 | // Subscribe to the ROS topic 162 | subs[url] = nh.subscribe(source, 1, boost::bind(&Image2RTSPNodelet::imageCallback, this, boost::placeholders::_1, url)); 163 | } 164 | num_of_clients[url]++; 165 | } 166 | } 167 | } 168 | 169 | void Image2RTSPNodelet::url_disconnected(string url) { 170 | std::string mountpoint; 171 | 172 | NODELET_INFO("Client disconnected: %s", url.c_str()); 173 | ros::NodeHandle& nh = getPrivateNodeHandle(); 174 | 175 | // Get the parameters from the rosparam server 176 | XmlRpc::XmlRpcValue streams; 177 | nh.getParam("streams", streams); 178 | ROS_ASSERT(streams.getType() == XmlRpc::XmlRpcValue::TypeStruct); 179 | 180 | // Go through and parse each stream 181 | for(XmlRpc::XmlRpcValue::ValueStruct::const_iterator it = streams.begin(); it != streams.end(); ++it) 182 | { 183 | XmlRpc::XmlRpcValue stream = streams[it->first]; 184 | mountpoint = static_cast(stream["mountpoint"]); 185 | 186 | // Check which stream the client has disconnected from 187 | if (url==mountpoint) { 188 | if (num_of_clients[url] > 0) num_of_clients[url]--; 189 | if (num_of_clients[url] == 0) { 190 | // No-one else is connected. Stop the subscription. 191 | subs[url].shutdown(); 192 | appsrc[url] = NULL; 193 | } 194 | } 195 | } 196 | } 197 | 198 | void Image2RTSPNodelet::print_info(char *s) { 199 | NODELET_INFO("%s",s); 200 | } 201 | 202 | void Image2RTSPNodelet::print_error(char *s) { 203 | NODELET_ERROR("%s",s); 204 | } 205 | 206 | PLUGINLIB_EXPORT_CLASS(image2rtsp::Image2RTSPNodelet, nodelet::Nodelet) 207 | --------------------------------------------------------------------------------