Coverage for backpack/rtsp.py: 0%

61 statements  

« 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. 

4 

5To use this class you MUST have the following dependencies correctly configured on your system: 

6 

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) 

10 

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. 

13 

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''' 

18 

19from typing import Any, Optional, List 

20import threading 

21import logging 

22 

23import gi 

24 

25gi.require_version('Gst', '1.0') 

26gi.require_version('GstRtspServer', '1.0') 

27from gi.repository import GLib, GstRtspServer 

28 

29from .skyline import SkyLine 

30 

31class RTSPServer: 

32 ''' The :class:`RTSPServer` instance wraps a GStreamer RTSP server that serves video streams 

33 to clients using the RTSP protocol. 

34 

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. 

39 

40 For an example usage of :class:`RTSPServer`, see the documentation of :class:`RTSPSkyLine` 

41 class. 

42 

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 ''' 

49 

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 

65 

66 def add_stream(self, mount_point: str, pipeline: str) -> None: 

67 ''' Registers a new video stream to the server. 

68 

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 

87 

88 def remove_stream(self, mount_point: str) -> None: 

89 ''' It removes a registered stream from the server. 

90 

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] 

97 

98 def start(self): 

99 ''' Starts the RTSP server asynchronously. 

100 

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() 

111 

112 def stop(self): 

113 ''' Stops the RTSP server. 

114 

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() 

119 

120 @property 

121 def port(self) -> str: 

122 ''' The port where this server listens to incoming connections. ''' 

123 return self._port 

124 

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()] 

128 

129 def __repr__(self) -> str: 

130 return f'<{self.__class__.__name__} streams=[{", ".join(self.urls())}]>' 

131 

132 

133 

134class RTSPSkyLine(SkyLine): 

135 ''' Together with :class:`RTSPServer`, :class:`RTSPSkyLine` a sequence of OpenCV frames 

136 on the RTSP protocol. 

137 

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:: 

142 

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() 

149 

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) 

155 

156 Using this code, you can access the streams at the following URLs: 

157 

158 - ``rtsp://127.0.0.1:8554/stream1`` 

159 - ``rtsp://127.0.0.1:8554/stream2`` 

160 

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. 

163 

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 ''' 

173 

174 LOCALHOST = '127.0.0.1' 

175 last_loopback_port = 5000 

176 

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()) 

184 

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 

199 

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