├── .gitignore ├── LICENSE ├── README.md ├── main_prg.py └── vid_streamv3.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | .idea/ 20 | 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | *.ver 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | #venv files 56 | venv/ 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 sahilparekh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GStreamer-Python 2 | 3 | This project helps in fetching continous live RTSP stream using GStreamer, Python and numpy without compromising on stream quality. 4 | 5 | **multiprocessing** is used in python to avoid main thread getting stuck. 6 | 7 | ## Getting Started 8 | 9 | Just clone this Repo then in main_prg.py add your rtsp stream on below line: 10 | 11 | ```python 12 | self.camlink = '' #Add your RTSP cam link 13 | ``` 14 | 15 | ### Prerequisites 16 | 17 | 1. Python 3 18 | 2. GStreamer 19 | 3. OpenCV (if you want to run this example as is) 20 | 4. Numpy 21 | 22 | ##### 1. Python 3 Installation 23 | This you would already know 24 | 25 | ##### 2. GStreamer Installation 26 | You will need GStreamer. Installation instruction can be found on this link [GStreamer](https://gstreamer.freedesktop.org/download/) 27 | Still for your quick reference will list installation instruction for Ubuntu: 28 | 29 | ``` 30 | apt-get install libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio 31 | ``` 32 | 33 | ##### 3. OpenCV Installation 34 | There are various way to install OpenCV but example using (Conda, PIP or build from source). But for purpose of this project below is instruction using PIP 35 | 36 | ``` 37 | pip3 install opencv-contrib-python 38 | ``` 39 | 40 | ##### 4. Numpy Installation 41 | ``` 42 | pip3 install numpy 43 | ``` 44 | 45 | ### Running the program 46 | 47 | Post cloning the Repo, go to repo dir (Also include cam link in main_prg.py as mentioned above). 48 | 49 | ```python 50 | python3 main_prg.py 51 | ``` 52 | 53 | ## License 54 | 55 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 56 | 57 | ### Star the REPO, if you find it useful. Feel free for pull requests. 58 | ## CHEERS!!! 59 | -------------------------------------------------------------------------------- /main_prg.py: -------------------------------------------------------------------------------- 1 | 2 | '''\ 3 | This Simple program Demonstrates how to use G-Streamer and capture RTSP Frames in Opencv using Python 4 | - Sahil Parekh 5 | ''' 6 | 7 | import multiprocessing as mp 8 | import time 9 | import vid_streamv3 as vs 10 | import cv2 11 | import sys 12 | 13 | ''' 14 | Main class 15 | ''' 16 | class mainStreamClass: 17 | def __init__(self): 18 | 19 | #Current Cam 20 | self.camProcess = None 21 | self.cam_queue = None 22 | self.stopbit = None 23 | self.camlink = '' #Add your RTSP cam link 24 | self.framerate = 6 25 | 26 | def startMain(self): 27 | 28 | #set queue size 29 | self.cam_queue = mp.Queue(maxsize=100) 30 | 31 | #get all cams 32 | time.sleep(3) 33 | 34 | self.stopbit = mp.Event() 35 | self.camProcess = vs.StreamCapture(self.camlink, 36 | self.stopbit, 37 | self.cam_queue, 38 | self.framerate) 39 | self.camProcess.start() 40 | 41 | # calculate FPS 42 | lastFTime = time.time() 43 | 44 | try: 45 | while True: 46 | 47 | if not self.cam_queue.empty(): 48 | # print('Got frame') 49 | cmd, val = self.cam_queue.get() 50 | 51 | ''' 52 | #calculate FPS 53 | diffTime = time.time() - lastFTime` 54 | fps = 1 / diffTime 55 | # print(fps) 56 | 57 | ''' 58 | lastFTime = time.time() 59 | 60 | # if cmd == vs.StreamCommands.RESOLUTION: 61 | # pass #print(val) 62 | 63 | if cmd == vs.StreamCommands.FRAME: 64 | if val is not None: 65 | cv2.imshow('Cam: ' + self.camlink, val) 66 | cv2.waitKey(1) 67 | 68 | except KeyboardInterrupt: 69 | print('Caught Keyboard interrupt') 70 | 71 | except: 72 | e = sys.exc_info() 73 | print('Caught Main Exception') 74 | print(e) 75 | 76 | self.stopCamStream() 77 | cv2.destroyAllWindows() 78 | 79 | 80 | def stopCamStream(self): 81 | print('in stopCamStream') 82 | 83 | if self.stopbit is not None: 84 | self.stopbit.set() 85 | while not self.cam_queue.empty(): 86 | try: 87 | _ = self.cam_queue.get() 88 | except: 89 | break 90 | self.cam_queue.close() 91 | 92 | self.camProcess.join() 93 | 94 | 95 | if __name__ == "__main__": 96 | mc = mainStreamClass() 97 | mc.startMain() -------------------------------------------------------------------------------- /vid_streamv3.py: -------------------------------------------------------------------------------- 1 | #cython: language_level=3, boundscheck=False 2 | import multiprocessing as mp 3 | from enum import Enum 4 | import numpy as np 5 | import gi 6 | gi.require_version('Gst', '1.0') 7 | from gi.repository import Gst 8 | Gst.init(None) 9 | 10 | '''Konwn issues 11 | 12 | * if format changes at run time system hangs 13 | ''' 14 | 15 | class StreamMode(Enum): 16 | INIT_STREAM = 1 17 | SETUP_STREAM = 1 18 | READ_STREAM = 2 19 | 20 | 21 | class StreamCommands(Enum): 22 | FRAME = 1 23 | ERROR = 2 24 | HEARTBEAT = 3 25 | RESOLUTION = 4 26 | STOP = 5 27 | 28 | 29 | class StreamCapture(mp.Process): 30 | 31 | def __init__(self, link, stop, outQueue, framerate): 32 | """ 33 | Initialize the stream capturing process 34 | link - rstp link of stream 35 | stop - to send commands to this process 36 | outPipe - this process can send commands outside 37 | """ 38 | 39 | super().__init__() 40 | self.streamLink = link 41 | self.stop = stop 42 | self.outQueue = outQueue 43 | self.framerate = framerate 44 | self.currentState = StreamMode.INIT_STREAM 45 | self.pipeline = None 46 | self.source = None 47 | self.decode = None 48 | self.convert = None 49 | self.sink = None 50 | self.image_arr = None 51 | self.newImage = False 52 | self.frame1 = None 53 | self.frame2 = None 54 | self.num_unexpected_tot = 40 55 | self.unexpected_cnt = 0 56 | 57 | 58 | 59 | def gst_to_opencv(self, sample): 60 | buf = sample.get_buffer() 61 | caps = sample.get_caps() 62 | 63 | # Print Height, Width and Format 64 | # print(caps.get_structure(0).get_value('format')) 65 | # print(caps.get_structure(0).get_value('height')) 66 | # print(caps.get_structure(0).get_value('width')) 67 | 68 | arr = np.ndarray( 69 | (caps.get_structure(0).get_value('height'), 70 | caps.get_structure(0).get_value('width'), 71 | 3), 72 | buffer=buf.extract_dup(0, buf.get_size()), 73 | dtype=np.uint8) 74 | return arr 75 | 76 | def new_buffer(self, sink, _): 77 | sample = sink.emit("pull-sample") 78 | arr = self.gst_to_opencv(sample) 79 | self.image_arr = arr 80 | self.newImage = True 81 | return Gst.FlowReturn.OK 82 | 83 | def run(self): 84 | # Create the empty pipeline 85 | self.pipeline = Gst.parse_launch( 86 | 'rtspsrc name=m_rtspsrc ! rtph264depay name=m_rtph264depay ! avdec_h264 name=m_avdech264 ! videoconvert name=m_videoconvert ! videorate name=m_videorate ! appsink name=m_appsink') 87 | 88 | # source params 89 | self.source = self.pipeline.get_by_name('m_rtspsrc') 90 | self.source.set_property('latency', 0) 91 | self.source.set_property('location', self.streamLink) 92 | self.source.set_property('protocols', 'tcp') 93 | self.source.set_property('retry', 50) 94 | self.source.set_property('timeout', 50) 95 | self.source.set_property('tcp-timeout', 5000000) 96 | self.source.set_property('drop-on-latency', 'true') 97 | 98 | # decode params 99 | self.decode = self.pipeline.get_by_name('m_avdech264') 100 | self.decode.set_property('max-threads', 2) 101 | self.decode.set_property('output-corrupt', 'false') 102 | 103 | # convert params 104 | self.convert = self.pipeline.get_by_name('m_videoconvert') 105 | 106 | #framerate parameters 107 | self.framerate_ctr = self.pipeline.get_by_name('m_videorate') 108 | self.framerate_ctr.set_property('max-rate', self.framerate/1) 109 | self.framerate_ctr.set_property('drop-only', 'true') 110 | 111 | # sink params 112 | self.sink = self.pipeline.get_by_name('m_appsink') 113 | 114 | # Maximum number of nanoseconds that a buffer can be late before it is dropped (-1 unlimited) 115 | # flags: readable, writable 116 | # Integer64. Range: -1 - 9223372036854775807 Default: -1 117 | self.sink.set_property('max-lateness', 500000000) 118 | 119 | # The maximum number of buffers to queue internally (0 = unlimited) 120 | # flags: readable, writable 121 | # Unsigned Integer. Range: 0 - 4294967295 Default: 0 122 | self.sink.set_property('max-buffers', 5) 123 | 124 | # Drop old buffers when the buffer queue is filled 125 | # flags: readable, writable 126 | # Boolean. Default: false 127 | self.sink.set_property('drop', 'true') 128 | 129 | # Emit new-preroll and new-sample signals 130 | # flags: readable, writable 131 | # Boolean. Default: false 132 | self.sink.set_property('emit-signals', True) 133 | 134 | # # sink.set_property('drop', True) 135 | # # sink.set_property('sync', False) 136 | 137 | # The allowed caps for the sink pad 138 | # flags: readable, writable 139 | # Caps (NULL) 140 | caps = Gst.caps_from_string( 141 | 'video/x-raw, format=(string){BGR, GRAY8}; video/x-bayer,format=(string){rggb,bggr,grbg,gbrg}') 142 | self.sink.set_property('caps', caps) 143 | 144 | if not self.source or not self.sink or not self.pipeline or not self.decode or not self.convert: 145 | print("Not all elements could be created.") 146 | self.stop.set() 147 | 148 | self.sink.connect("new-sample", self.new_buffer, self.sink) 149 | 150 | # Start playing 151 | ret = self.pipeline.set_state(Gst.State.PLAYING) 152 | if ret == Gst.StateChangeReturn.FAILURE: 153 | print("Unable to set the pipeline to the playing state.") 154 | self.stop.set() 155 | 156 | # Wait until error or EOS 157 | bus = self.pipeline.get_bus() 158 | 159 | while True: 160 | 161 | if self.stop.is_set(): 162 | print('Stopping CAM Stream by main process') 163 | break 164 | 165 | message = bus.timed_pop_filtered(10000, Gst.MessageType.ANY) 166 | # print "image_arr: ", image_arr 167 | if self.image_arr is not None and self.newImage is True: 168 | 169 | if not self.outQueue.full(): 170 | 171 | # print("\r adding to queue of size{}".format(self.outQueue.qsize()), end='\r') 172 | self.outQueue.put((StreamCommands.FRAME, self.image_arr), block=False) 173 | 174 | self.image_arr = None 175 | self.unexpected_cnt = 0 176 | 177 | 178 | if message: 179 | if message.type == Gst.MessageType.ERROR: 180 | err, debug = message.parse_error() 181 | print("Error received from element %s: %s" % ( 182 | message.src.get_name(), err)) 183 | print("Debugging information: %s" % debug) 184 | break 185 | elif message.type == Gst.MessageType.EOS: 186 | print("End-Of-Stream reached.") 187 | break 188 | elif message.type == Gst.MessageType.STATE_CHANGED: 189 | if isinstance(message.src, Gst.Pipeline): 190 | old_state, new_state, pending_state = message.parse_state_changed() 191 | print("Pipeline state changed from %s to %s." % 192 | (old_state.value_nick, new_state.value_nick)) 193 | else: 194 | print("Unexpected message received.") 195 | self.unexpected_cnt = self.unexpected_cnt + 1 196 | if self.unexpected_cnt == self.num_unexpected_tot: 197 | break 198 | 199 | 200 | print('terminating cam pipe') 201 | self.stop.set() 202 | self.pipeline.set_state(Gst.State.NULL) --------------------------------------------------------------------------------