├── requirements.txt ├── ubuntu_packages.txt ├── README.md ├── LICENSE ├── .gitignore ├── main.py └── h264.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pygobject==3.26.1 2 | -------------------------------------------------------------------------------- /ubuntu_packages.txt: -------------------------------------------------------------------------------- 1 | python3-gi 2 | python3-gst-1.0 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # H.264_enc-dec 2 | A Python library based on gstreamer-1.0 for encoding and decoding RTP H.264 streams. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ivan Lysenko 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | 4 | from h264 import * 5 | 6 | # Example usage of the library 7 | 8 | if __name__ == '__main__': 9 | Gst.init(None) 10 | 11 | # ===========Recording and encoding 5 seconds of footage from the webcam=========== 12 | 13 | encoder = H264_Encoder() 14 | 15 | pipeline = Gst.Pipeline.new(None) 16 | # v4l2src ! image/jpeg,width=1280,height=720,framerate=30/1 ! jpegdec ! rawvideoparse width=1280 height=720 framerate=30/1 ! appsink 17 | 18 | webcam = Gst.ElementFactory.make('v4l2src') 19 | 20 | caps = Gst.Caps.from_string('image/jpeg,width=1280,height=720,framerate=30/1') 21 | capsfilter = Gst.ElementFactory.make('capsfilter') 22 | capsfilter.set_property('caps', caps) 23 | 24 | jpegdec = Gst.ElementFactory.make('jpegdec') 25 | 26 | videoparse = Gst.ElementFactory.make('rawvideoparse') 27 | videoparse.set_property('width', 1280) 28 | videoparse.set_property('height', 720) 29 | videoparse.set_property('framerate', Gst.Fraction(30)) 30 | 31 | appsink = Gst.ElementFactory.make('appsink') 32 | sinkcaps = Gst.Caps.from_string( 33 | 'video/x-raw,format=I420,width=1280,height=720,framerate=30/1' 34 | ) 35 | appsink.set_property('caps', sinkcaps) 36 | appsink.set_property('drop', True) 37 | appsink.set_property('emit-signals', True) 38 | webcam_frames = [] 39 | payloads = [] 40 | 41 | def get_appsink_data(sink): 42 | sample = sink.emit('pull-sample') 43 | if not sample: 44 | return 45 | buf = sample.get_buffer() 46 | _, info = buf.map(Gst.MapFlags.READ) 47 | frame = VideoFrame(1280, 720, info.data) 48 | webcam_frames.append(frame) 49 | payloads.extend(encoder.encode(frame)) 50 | buf.unmap(info) 51 | 52 | return Gst.FlowReturn.OK 53 | 54 | appsink.connect('new-sample', get_appsink_data) 55 | 56 | pipeline.add(webcam) 57 | pipeline.add(capsfilter) 58 | pipeline.add(jpegdec) 59 | pipeline.add(videoparse) 60 | pipeline.add(appsink) 61 | 62 | webcam.link(capsfilter) 63 | capsfilter.link(jpegdec) 64 | jpegdec.link(videoparse) 65 | videoparse.link(appsink) 66 | 67 | pipeline.set_state(Gst.State.PLAYING) 68 | time.sleep(5) 69 | pipeline.set_state(Gst.State.NULL) 70 | 71 | print('Encoded 5 seconds of webcam footage!') 72 | print(len(webcam_frames), 'frames and', len(payloads), 'payloads in total.') 73 | print('Decoding now...') 74 | 75 | # ===========Decoding the encoded webcam footage=========== 76 | 77 | decoder = H264_Decoder() 78 | decoded_frames = decoder.decode(payloads) 79 | 80 | print('Decoded!', len(decoded_frames), 'frames in total.') 81 | 82 | if not os.path.isdir('cam'): 83 | os.mkdir('cam') 84 | 85 | print('Writing original frame #50...') 86 | with open('cam/original.yuv', 'wb') as file: 87 | file.write(webcam_frames[50].data) 88 | 89 | print('Writing decoded frame #50...') 90 | with open('cam/decoded.yuv', 'wb') as file: 91 | file.write(decoded_frames[50].data) 92 | 93 | # ===========Displaying the decoded footage=========== 94 | 95 | print('Displaying the decoded result...') 96 | 97 | pipeline = Gst.Pipeline.new(None) 98 | # appsrc -> rawvideoparse -> xvimagesink 99 | 100 | appsrc = Gst.ElementFactory.make('appsrc') 101 | srccaps = Gst.Caps.from_string('video/x-raw,format=I420,width=1280,height=720,framerate=30/1') 102 | appsrc.set_property('caps', srccaps) 103 | 104 | def frame_generator(): 105 | for frame in decoded_frames: 106 | yield frame.data 107 | 108 | generator = frame_generator() 109 | 110 | def feed_appsrc(bus, msg): 111 | try: 112 | frame = next(generator) 113 | buf = Gst.Buffer.new_wrapped(frame) 114 | appsrc.emit('push-buffer', buf) 115 | except StopIteration: 116 | appsrc.emit('end-of-stream') 117 | 118 | appsrc.connect('need-data', feed_appsrc) 119 | 120 | videoparse = Gst.ElementFactory.make('rawvideoparse') 121 | videoparse.set_property('width', 1280) 122 | videoparse.set_property('height', 720) 123 | videoparse.set_property('framerate', Gst.Fraction(30)) 124 | 125 | xvimagesink = Gst.ElementFactory.make('xvimagesink') 126 | 127 | pipeline.add(appsrc) 128 | pipeline.add(videoparse) 129 | pipeline.add(xvimagesink) 130 | 131 | appsrc.link(videoparse) 132 | videoparse.link(xvimagesink) 133 | 134 | pipeline.set_state(Gst.State.PLAYING) 135 | bus = pipeline.get_bus() 136 | bus.timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS) 137 | pipeline.set_state(Gst.State.NULL) 138 | -------------------------------------------------------------------------------- /h264.py: -------------------------------------------------------------------------------- 1 | import gi 2 | gi.require_version('Gst', '1.0') 3 | from gi.repository import Gst 4 | 5 | import math 6 | from abc import ABC, abstractmethod 7 | 8 | MAX_BUFFERS = 100 9 | 10 | class VideoFrame: 11 | def __init__(self, width, height, data=None): 12 | self.width = width 13 | self.height = height 14 | 15 | if data is None: 16 | self.data = b'\x00' * math.ceil(width * height * 12 / 8) 17 | else: 18 | self.data = data 19 | 20 | ############################################################################## 21 | 22 | class H264_Exception(Exception): # made for easy catching of both types 23 | pass 24 | 25 | class H264_Encoder_Exception(H264_Exception): 26 | pass 27 | 28 | class H264_Decoder_Exception(H264_Exception): 29 | pass 30 | 31 | ############################################################################## 32 | 33 | class H264_Superclass(ABC): 34 | def error(self, err_msg): 35 | if type(self) == H264_Encoder: 36 | raise H264_Encoder_Exception(err_msg) 37 | elif type(self) == H264_Decoder: 38 | raise H264_Decoder_Exception(err_msg) 39 | else: 40 | raise Exception(err_msg) 41 | 42 | def change_state(self, state): 43 | state = self.pipeline.set_state(state) 44 | if state == Gst.StateChangeReturn.FAILURE: 45 | self.error('Failed to change pipeline\'s state to ' + str(state)) 46 | 47 | def wait_for_pipeline(self): 48 | msg = self.pipeline.get_bus().timed_pop_filtered(Gst.CLOCK_TIME_NONE, 49 | Gst.MessageType.ERROR | Gst.MessageType.EOS) 50 | if msg: 51 | if msg.type == Gst.MessageType.ERROR: 52 | err, _ = msg.parse_error() 53 | self.error('Pipeline failure: ' + err.message) 54 | elif msg.type != Gst.MessageType.EOS: 55 | self.error('Pipeline failure: unknown error') 56 | 57 | def __init__(self): 58 | self.frames = [] 59 | self.payloads = [] 60 | 61 | self.create_pipeline() 62 | 63 | self.change_state(Gst.State.READY) 64 | 65 | super().__init__() 66 | 67 | def __del__(self): 68 | self.pipeline.set_state(Gst.State.NULL) 69 | 70 | @abstractmethod 71 | def create_pipeline(self): 72 | pass 73 | 74 | ############################################################################## 75 | 76 | class H264_Encoder(H264_Superclass): 77 | def __init__(self): 78 | self.last_parameters = (0, 0) 79 | super().__init__() 80 | 81 | @staticmethod 82 | def create_srccaps(width, height): 83 | CAPS_STR = 'video/x-raw,format=I420,width={},height={},framerate=0/1' 84 | return Gst.Caps.from_string(CAPS_STR.format(width, height)) 85 | 86 | def create_pipeline(self): 87 | self.pipeline = Gst.Pipeline.new() 88 | # appsrc -> rawvideoparse -> videoconvert -> x264enc -> rtph264pay -> appsink 89 | 90 | self.appsrc = Gst.ElementFactory.make('appsrc') 91 | self.appsrc.set_property('caps', self.create_srccaps(0, 0)) 92 | 93 | def feed_appsrc(bus, msg): 94 | if len(self.frames) == 0: 95 | self.appsrc.emit('end-of-stream') 96 | else: 97 | buf = Gst.Buffer.new_wrapped(self.frames[0].data) 98 | self.appsrc.emit('push-buffer', buf) 99 | del(self.frames[0]) 100 | 101 | self.appsrc.connect('need-data', feed_appsrc) 102 | 103 | self.videoparse = Gst.ElementFactory.make('rawvideoparse') 104 | self.videoparse.set_property('width', 0) 105 | self.videoparse.set_property('height', 0) 106 | self.videoparse.set_property('framerate', Gst.Fraction(0)) 107 | 108 | videoconvert = Gst.ElementFactory.make('videoconvert') 109 | x264_encoder = Gst.ElementFactory.make('x264enc') 110 | rtp_payloader = Gst.ElementFactory.make('rtph264pay') 111 | 112 | self.appsink = Gst.ElementFactory.make('appsink') 113 | rtpcaps = Gst.Caps.from_string( 114 | 'application/x-rtp,payload=96,media=video,encoding-name=H264,clock-rate=90000' 115 | ) 116 | self.appsink.set_property('caps', rtpcaps) 117 | self.appsink.set_property('drop', True) # should we drop?? 118 | self.appsink.set_property('max-buffers', MAX_BUFFERS) 119 | self.appsink.set_property('emit-signals', True) 120 | 121 | def get_appsink_data(sink): 122 | sample = sink.emit('pull-sample') 123 | if not sample: 124 | return 125 | buf = sample.get_buffer() 126 | status, info = buf.map(Gst.MapFlags.READ) 127 | if not status: 128 | self.error('Failed to map buffer data to GstMapInfo') 129 | self.payloads.append(info.data) 130 | buf.unmap(info) 131 | 132 | return Gst.FlowReturn.OK 133 | 134 | self.appsink.connect('new-sample', get_appsink_data) 135 | 136 | self.pipeline.add(self.appsrc) 137 | self.pipeline.add(self.videoparse) 138 | self.pipeline.add(videoconvert) 139 | self.pipeline.add(x264_encoder) 140 | self.pipeline.add(rtp_payloader) 141 | self.pipeline.add(self.appsink) 142 | 143 | self.appsrc.link(self.videoparse) 144 | self.videoparse.link(videoconvert) 145 | videoconvert.link(x264_encoder) 146 | x264_encoder.link(rtp_payloader) 147 | rtp_payloader.link(self.appsink) 148 | 149 | def update_parameters(self, width, height): 150 | if not self.last_parameters or self.last_parameters != (width, height): 151 | self.appsrc.set_property('caps', self.create_srccaps(width, height)) 152 | 153 | self.videoparse.set_property('width', width) 154 | self.videoparse.set_property('height', height) 155 | 156 | self.last_parameters = (width, height) 157 | 158 | ''' 159 | Encodes raw YUV420 video frames with H.264 and packages the result in RTP payloads 160 | 161 | :param frames: list of VideoFrame objects with the *same* width and height / single VideoFrame object 162 | :returns: list of binary representations of RTP payloads 163 | ''' 164 | def encode(self, frames): 165 | if type(frames) == VideoFrame: 166 | frames = [frames] 167 | 168 | if len(frames) == 0: 169 | self.error('\'frames\' length should be greater than 0') 170 | 171 | self.frames = frames 172 | self.update_parameters(frames[0].width, frames[0].height) 173 | self.change_state(Gst.State.PLAYING) 174 | 175 | self.wait_for_pipeline() 176 | 177 | self.change_state(Gst.State.READY) 178 | 179 | current_payloads = self.payloads 180 | 181 | self.frames = [] 182 | self.payloads = [] 183 | 184 | return current_payloads 185 | 186 | ############################################################################## 187 | 188 | class H264_Decoder(H264_Superclass): 189 | def create_pipeline(self): 190 | self.pipeline = Gst.Pipeline.new() 191 | # appsrc -> rtph264depay -> h264parse -> avdec_h264 -> videoconvert -> appsink 192 | 193 | self.appsrc = Gst.ElementFactory.make('appsrc') 194 | self.appsrc.set_property('format', Gst.Format.TIME) 195 | rtpcaps = Gst.Caps.from_string( 196 | 'application/x-rtp,payload=96,media=video,encoding-name=H264,clock-rate=90000' 197 | ) 198 | self.appsrc.set_property('caps', rtpcaps) 199 | 200 | def feed_appsrc(bus, msg): 201 | if len(self.payloads) == 0: 202 | self.appsrc.emit('end-of-stream') 203 | else: 204 | buf = Gst.Buffer.new_wrapped(self.payloads[0]) 205 | self.appsrc.emit('push-buffer', buf) 206 | del(self.payloads[0]) 207 | 208 | self.appsrc.connect('need-data', feed_appsrc) 209 | 210 | rtp_depayloader = Gst.ElementFactory.make('rtph264depay') 211 | h264_parser = Gst.ElementFactory.make('h264parse') 212 | h264_decoder = Gst.ElementFactory.make('avdec_h264') 213 | videoconvert = Gst.ElementFactory.make('videoconvert') 214 | 215 | self.appsink = Gst.ElementFactory.make('appsink') 216 | self.appsink.set_property('drop', True) # should we drop?? 217 | self.appsink.set_property('max-buffers', MAX_BUFFERS) 218 | self.appsink.set_property('emit-signals', True) 219 | 220 | def get_appsink_data(sink): 221 | sample = sink.emit('pull-sample') 222 | if not sample: 223 | return 224 | buf = sample.get_buffer() 225 | status, info = buf.map(Gst.MapFlags.READ) 226 | if not status: 227 | self.error('Failed to map buffer data to GstMapInfo') 228 | self.frames.append(VideoFrame(0, 0, info.data)) 229 | buf.unmap(info) 230 | 231 | return Gst.FlowReturn.OK 232 | 233 | self.appsink.connect('new-sample', get_appsink_data) 234 | 235 | self.pipeline.add(self.appsrc) 236 | self.pipeline.add(rtp_depayloader) 237 | self.pipeline.add(h264_parser) 238 | self.pipeline.add(h264_decoder) 239 | self.pipeline.add(videoconvert) 240 | self.pipeline.add(self.appsink) 241 | 242 | self.appsrc.link(rtp_depayloader) 243 | rtp_depayloader.link(h264_parser) 244 | h264_parser.link(h264_decoder) 245 | h264_decoder.link(videoconvert) 246 | videoconvert.link(self.appsink) 247 | 248 | def update_frames_sizes(self): 249 | pad = self.appsink.get_static_pad('sink') 250 | caps = pad.get_current_caps() 251 | if caps is None: 252 | self.error('Appsink caps is somehow None - report this') 253 | structure = caps.get_structure(0) 254 | if structure is None: 255 | self.error('Appsink caps structure is somehow None - report this') 256 | 257 | w_status, width = structure.get_int('width') 258 | h_status, height = structure.get_int('height') 259 | 260 | if not w_status or not h_status: 261 | self.error('Could not extract frame width and height from appsink') 262 | 263 | for frame in self.frames: 264 | frame.width = width 265 | frame.height = height 266 | 267 | ''' 268 | Decodes H.264 RTP payloads to a list of raw YUV420 frames 269 | 270 | :param payloads: list of binary representations of RTP payloads 271 | :returns: list of VideoFrame objects 272 | ''' 273 | def decode(self, payloads): 274 | if len(payloads) == 0: 275 | self.error('\'payloads\' length should be greater than 0') 276 | 277 | self.payloads = payloads 278 | 279 | self.change_state(Gst.State.PLAYING) 280 | 281 | self.wait_for_pipeline() 282 | 283 | self.update_frames_sizes() 284 | 285 | self.change_state(Gst.State.READY) 286 | 287 | current_frames = self.frames 288 | 289 | self.payloads = [] 290 | self.frames = [] 291 | 292 | return current_frames 293 | --------------------------------------------------------------------------------