Coverage for backpack/rtsp.py: 0%
61 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 23:12 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 23:12 +0000
1'''
2This module contains RTSPSkyLine and RTSPServer. These two classes allow serving a sequence of
3OpenCV images as video streams using the RTSP protocol.
5To use this class you MUST have the following dependencies correctly configured on your system:
7 - `GStreamer 1.0`_ installed with standard plugins pack, libav, tools and development libraries
8 - `OpenCV 4.2.0`_, compiled with GStreamer support and Python bindings
9 - `gst-rtsp-server`_ with development libraries (libgstrtspserver-1.0-dev)
11These dependencies can not be easily specified by a ``requirements.txt`` or a Conda environment.
12See the example ``Dockerfile`` on how to install these dependencies on your system.
14.. _`GStreamer 1.0`: https://gstreamer.freedesktop.org
15.. _`OpenCV 4.2.0`: https://opencv.org/opencv-4-2-0/
16.. _`gst-rtsp-server`: https://github.com/GStreamer/gst-rtsp-server
17'''
19from typing import Any, Optional, List
20import threading
21import logging
23import gi
25gi.require_version('Gst', '1.0')
26gi.require_version('GstRtspServer', '1.0')
27from gi.repository import GLib, GstRtspServer
29from .skyline import SkyLine
31class RTSPServer:
32 ''' The :class:`RTSPServer` instance wraps a GStreamer RTSP server that serves video streams
33 to clients using the RTSP protocol.
35 You typically want to have a single instance of :class:`RTSPServer` for your application.
36 You can register any number of video streams that will be served by a single instance of
37 RTSP server. The port number of the server should be unique among all applications on the
38 device.
40 For an example usage of :class:`RTSPServer`, see the documentation of :class:`RTSPSkyLine`
41 class.
43 Args:
44 port: The port to listen on. You can not modify the port after the initialization of
45 the :class:`RTSPServer` instance. Defaults to ``8554``.
46 parent_logger: If you want to connect the logger of :class:`RTSPServer` to a parent,
47 specify it here.
48 '''
50 def __init__(
51 self,
52 port: str = '8554',
53 parent_logger: Optional[logging.Logger] = None,
54 ):
55 self.logger = (
56 logging.getLogger(self.__class__.__name__) if parent_logger is None else
57 parent_logger.getChild(self.__class__.__name__)
58 )
59 self.gst_server = GstRtspServer.RTSPServer()
60 self.gst_server.service = port
61 self.streams = {}
62 self._port = port
63 self._loop = GLib.MainLoop()
64 self._thread = None
66 def add_stream(self, mount_point: str, pipeline: str) -> None:
67 ''' Registers a new video stream to the server.
69 Args:
70 mount_point: The path that will be used to access the stream. For example,
71 if you specify ``/my_stream``, the stream will be accessible for clients using
72 the ``rtsp://127.0.0.1:8854/mystream`` url (change the IP address and the port
73 number accordingly).
74 pipeline: The GStreamer pipeline to use for the stream. This will be typically
75 a pipeline picking up the raw UDP packets from a local port and wrapping it to a
76 H.264 envelope, for example:
77 ``udpsrc port=5000 ! application/x-rtp,media=video,encoding-name=H264 ! rtph264depay !
78 rtph264pay name=pay0``
79 '''
80 self.logger.info('Adding pipeline to mount point "%s"', mount_point)
81 mounts = self.gst_server.get_mount_points()
82 factory = GstRtspServer.RTSPMediaFactory()
83 factory.set_launch(pipeline)
84 factory.set_shared(True)
85 mounts.add_factory(mount_point, factory)
86 self.streams[mount_point] = pipeline
88 def remove_stream(self, mount_point: str) -> None:
89 ''' It removes a registered stream from the server.
91 Args:
92 mount_point (str): The registered path of the stream.
93 '''
94 mounts = self.gst_server.get_mount_points()
95 mounts.remove_factory(mount_point)
96 del self.streams[mount_point]
98 def start(self):
99 ''' Starts the RTSP server asynchronously.
101 After calling this method, the RTSP requests will be served.
102 '''
103 self.logger.info('Starting RTSPServer')
104 self.gst_server.attach()
105 self._thread = threading.Thread(
106 name='RTSPServerThread',
107 target=self._loop.run,
108 daemon=True
109 )
110 self._thread.start()
112 def stop(self):
113 ''' Stops the RTSP server.
115 After calling this method, no RTSP requests will be served. The server can be restarted
116 later on calling the :meth:`start()` method.
117 '''
118 self._loop.quit()
120 @property
121 def port(self) -> str:
122 ''' The port where this server listens to incoming connections. '''
123 return self._port
125 def urls(self) -> List[str]:
126 ''' Returns the list of URLs where the server serves RTSP streams. '''
127 return [f'rtsp://127.0.0.1:{self.port}{mp}' for mp in self.streams.keys()]
129 def __repr__(self) -> str:
130 return f'<{self.__class__.__name__} streams=[{", ".join(self.urls())}]>'
134class RTSPSkyLine(SkyLine):
135 ''' Together with :class:`RTSPServer`, :class:`RTSPSkyLine` a sequence of OpenCV frames
136 on the RTSP protocol.
138 A single instance of :class:`RTSPServer` application can serve streams coming from multiple
139 :class:`RTSPSkyLine` instances. You should instantiate the :class:`RTSPServer` instance first.
140 For example, if you want to serve two separate RTSP streams, you could use this code to set up
141 your scenario::
143 server = RTSPServer(port="8554")
144 skyline1 = RTSPSkyLine(server, "/stream1")
145 skyline2 = RTSPSkyLine(server, "/stream2")
146 skyline1.start_streaming(30, 640, 480)
147 skyline2.start_streaming(30, 640, 480)
148 server.start()
150 while True:
151 frame1 = ... # Get frame for the first stream as a numpy array of shape (640, 480, 3)
152 frame2 = ... # Get frame for the second stream
153 skyline1.put(frame1)
154 skyline2.put(frame2)
156 Using this code, you can access the streams at the following URLs:
158 - ``rtsp://127.0.0.1:8554/stream1``
159 - ``rtsp://127.0.0.1:8554/stream2``
161 If the application (or the firewall) is configured to allow incoming connections on the ``8554``
162 port, the streams will be accessibly also from the external ip of the device.
164 Args:
165 server: The RTSPServer instance that this stream is being served by
166 path: The path to the stream. This is the path that the client will use to connect to
167 the stream
168 args: Positional arguments to be passed to :class:`~backpack.skyline.SkyLine`
169 superclass initializer.
170 kwargs: Keyword arguments to be passed to :class:`~backpack.skyline.SkyLine`
171 superclass initializer.
172 '''
174 LOCALHOST = '127.0.0.1'
175 last_loopback_port = 5000
177 def __init__(self, server: RTSPServer, path: str, *args: Any, **kwargs: Any) -> None:
178 super().__init__(*args, **kwargs)
179 self.server = server
180 self.path = path if path.startswith('/') else '/' + path
181 self.loopback_port = RTSPSkyLine.last_loopback_port
182 RTSPSkyLine.last_loopback_port += 1
183 self.server.add_stream(self.path, self._get_server_pipeline())
185 def _get_pipeline(self, fps: float, width: int, height: int) -> str:
186 pipeline = ' ! '.join([
187 'appsrc',
188 'queue',
189 'videoconvert',
190 f'video/x-raw,format=I420,width={width},height={height},framerate={fps}/1',
191 'x264enc bframes=0 key-int-max=45 bitrate=500',
192 'video/x-h264,stream-format=avc,alignment=au,profile=baseline',
193 'h264parse',
194 'rtph264pay',
195 f'udpsink host={self.LOCALHOST} port={self.loopback_port}'
196 ])
197 self.logger.info(f'GStreamer application pipeline definition:\n{pipeline}')
198 return pipeline
200 def _get_server_pipeline(self):
201 pipeline = ' ! '.join([
202 f'udpsrc port={self.loopback_port}',
203 'application/x-rtp,media=video,encoding-name=H264',
204 'rtph264depay',
205 'rtph264pay name=pay0'
206 ])
207 self.logger.info(f'GStreamer RTSP server pipeline definition:\n{pipeline}')
208 return pipeline