├── README.md ├── .gitignore ├── LICENSE ├── index.html └── cam_server.py /README.md: -------------------------------------------------------------------------------- 1 | gstreamer-webcam_to_browser 2 | =========================== 3 | 4 | Realtime webcam stream to a browser window using Python, Tornado, WebSockets, and Gstreamer 5 | 6 | Dependencies: 7 | ============= 8 | 9 | - Python 10 | - Tornado (pip install tornado) 11 | - GStreamer 1.0+ 12 | - GStreamer Python bindings 13 | 14 | To run, simply enter: 15 | 16 | python cam_server.py 17 | 18 | in your Linux terminal. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Charles Kiorpes 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 | 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Test 6 | 7 | 164 | 165 |

WebSocket Test

166 | 167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /cam_server.py: -------------------------------------------------------------------------------- 1 | import tornado 2 | import tornado.websocket 3 | import tornado.httpserver 4 | import threading 5 | import time 6 | import base64 7 | import sys, os 8 | import gi 9 | gi.require_version('Gst', '1.0') 10 | from gi.repository import Gst, GObject 11 | import json 12 | import signal 13 | 14 | cam_sockets = [] 15 | key_sockets = [] 16 | 17 | frame_grabber = None 18 | 19 | #import Adafruit_BBIO.GPIO as GPIO 20 | #import Adafruit_BBIO.PWM as PWM 21 | 22 | DEAD_ZONE = 10 23 | FORWARD_SPEED = 75.0 24 | TURN_SPEED = 50.0 25 | FORWARD = 1 26 | BACKWARD = -1 27 | LEFT = 0 28 | RIGHT = 1 29 | 30 | """ 31 | Output pins for the motor driver board: 32 | """ 33 | motor_pwms = ["P9_14", "P9_21"] 34 | motor_ins = [["P9_11", "P9_12"], ["P9_25", "P9_26"]] 35 | STBY = "P9_27" 36 | 37 | def init_motors(): 38 | """ 39 | Initialize the pins needed for the motor driver. 40 | """ 41 | """global motor_ins 42 | global motor_pwms 43 | # initialize GPIO pins 44 | GPIO.setup(STBY, GPIO.OUT) 45 | GPIO.output(STBY, GPIO.HIGH) 46 | for motor in motor_ins: 47 | for pin in motor: 48 | GPIO.setup(pin, GPIO.OUT) 49 | GPIO.output(pin, GPIO.LOW) 50 | # initialize PWM pins 51 | # first need bogus start due to unknown bug in library 52 | PWM.start("P9_14", 0.0) 53 | PWM.stop("P9_14") 54 | # now start the desired PWMs 55 | for pwm_pin in motor_pwms: 56 | PWM.start(pwm_pin, 0.0)""" 57 | 58 | def set_motor(motor, direction, value): 59 | """ 60 | Set an individual motor's direction and speed 61 | """ 62 | """if direction == BACKWARD: # For now, assume CW is forwards 63 | # forwards: in1 LOW, in2 HIGH 64 | GPIO.output(motor_ins[motor][0], GPIO.LOW) 65 | GPIO.output(motor_ins[motor][1], GPIO.HIGH) 66 | elif direction == FORWARD: 67 | GPIO.output(motor_ins[motor][0], GPIO.HIGH) 68 | GPIO.output(motor_ins[motor][1], GPIO.LOW) 69 | else: 70 | # there has been an error, stop motors 71 | GPIO.output(STBY, GPIO.LOW) 72 | PWM.set_duty_cycle(motor_pwms[motor], value)""" 73 | 74 | 75 | def parse_command_vector(s): 76 | """left_speed = 0.0 77 | left_dir = FORWARD 78 | right_speed = 0.0 79 | right_dir = FORWARD 80 | if s[1] == 1: 81 | print('UP') 82 | left_speed = FORWARD_SPEED 83 | right_speed = FORWARD_SPEED 84 | if s[2] == 1: 85 | print('DOWN') 86 | left_speed = FORWARD_SPEED 87 | right_speed = FORWARD_SPEED 88 | left_dir = BACKWARD 89 | right_dir = BACKWARD 90 | if s[3] == 1: 91 | print('LEFT') 92 | left_speed = FORWARD_SPEED 93 | right_speed = FORWARD_SPEED 94 | left_dir = BACKWARD 95 | right_dir = FORWARD 96 | if s[4] == 1: 97 | print('RIGHT') 98 | left_dir = FORWARD 99 | left_speed = FORWARD_SPEED 100 | right_speed = FORWARD_SPEED 101 | right_dir = BACKWARD 102 | 103 | set_motor(LEFT, left_dir, left_speed) 104 | set_motor(RIGHT, right_dir, right_speed)""" 105 | 106 | 107 | 108 | def send_all(msg): 109 | for ws in cam_sockets: 110 | ws.write_message(msg, True) 111 | 112 | class CamWSHandler(tornado.websocket.WebSocketHandler): 113 | def open(self): 114 | global cam_sockets 115 | cam_sockets.append(self) 116 | print('new camera connection') 117 | 118 | def on_message(self, message): 119 | print (message) 120 | 121 | def on_close(self): 122 | global cam_sockets 123 | cam_sockets.remove(self) 124 | print('camera connection closed') 125 | 126 | def check_origin(self, origin): 127 | return True 128 | 129 | class KeyWSHandler(tornado.websocket.WebSocketHandler): 130 | def open(self): 131 | global key_sockets 132 | key_sockets.append(self) 133 | print('new command connection') 134 | 135 | def on_message(self, message): 136 | print (message) 137 | parse_command_vector(json.loads(message)) 138 | 139 | def on_close(self): 140 | global key_sockets 141 | key_sockets.remove(self) 142 | print('command connection closed') 143 | 144 | def check_origin(self, origin): 145 | return True 146 | 147 | class HTTPServer(tornado.web.RequestHandler): 148 | def get(self): 149 | self.render("index.html") 150 | 151 | class MainPipeline(): 152 | def __init__(self): 153 | self.pipeline = None 154 | self.videosrc = None 155 | self.videoparse = None 156 | self.videosink = None 157 | self.current_buffer = None 158 | 159 | def pull_frame(self, sink): 160 | # second param appears to be the sink itself 161 | sample = self.videosink.emit("pull-sample") 162 | if sample is not None: 163 | self.current_buffer = sample.get_buffer() 164 | current_data = self.current_buffer.extract_dup(0, self.current_buffer.get_size()) 165 | send_all(current_data) 166 | return False 167 | 168 | def gst_thread(self): 169 | print("Initializing GST Elements") 170 | Gst.init(None) 171 | 172 | self.pipeline = Gst.Pipeline.new("framegrabber") 173 | 174 | # instantiate the camera source 175 | self.videosrc = Gst.ElementFactory.make("v4l2src", "vid-src") 176 | self.videosrc.set_property("device", "/dev/video0") 177 | 178 | # instantiate the jpeg parser to ensure whole frames 179 | self.videoparse = Gst.ElementFactory.make("jpegparse", "vid-parse") 180 | 181 | # instantiate the appsink - allows access to raw frame data 182 | self.videosink = Gst.ElementFactory.make("appsink", "vid-sink") 183 | self.videosink.set_property("max-buffers", 3) 184 | self.videosink.set_property("drop", True) 185 | self.videosink.set_property("emit-signals", True) 186 | self.videosink.set_property("sync", False) 187 | self.videosink.connect("new-sample", self.pull_frame) 188 | 189 | # add all the new elements to the pipeline 190 | print("Adding Elements to Pipeline") 191 | self.pipeline.add(self.videosrc) 192 | self.pipeline.add(self.videoparse) 193 | self.pipeline.add(self.videosink) 194 | 195 | # link the elements in order, adding a filter to ensure correct size and framerate 196 | print("Linking GST Elements") 197 | self.videosrc.link_filtered(self.videoparse, 198 | Gst.caps_from_string('image/jpeg,width=640,height=480,framerate=30/1')) 199 | self.videoparse.link(self.videosink) 200 | 201 | # start the video 202 | print("Setting Pipeline State") 203 | self.pipeline.set_state(Gst.State.PAUSED) 204 | self.pipeline.set_state(Gst.State.PLAYING) 205 | 206 | def start_server(cam_app, key_app): 207 | cam_server = tornado.httpserver.HTTPServer(cam_app) 208 | key_server = tornado.httpserver.HTTPServer(key_app) 209 | cam_server.listen(8888) 210 | key_server.listen(8889) 211 | tornado.ioloop.IOLoop.instance().start() 212 | 213 | def signal_handler(signum, frame): 214 | print("Interrupt caught") 215 | tornado.ioloop.IOLoop.instance().stop() 216 | server_thread.stop() 217 | 218 | if __name__ == "__main__": 219 | 220 | init_motors() 221 | 222 | cam_app = tornado.web.Application([ 223 | (r'/ws', CamWSHandler), 224 | (r'/', HTTPServer), 225 | ]) 226 | 227 | key_app = tornado.web.Application([ 228 | (r'/ws', KeyWSHandler) 229 | ]) 230 | 231 | 232 | print("Starting GST thread...") 233 | 234 | pipeline = MainPipeline() 235 | gst_thread = threading.Thread(target=pipeline.gst_thread) 236 | gst_thread.start() 237 | 238 | time.sleep(1) 239 | 240 | print("starting frame grabber thread") 241 | 242 | print("Starting server thread") 243 | server_thread = threading.Thread(target=start_server, args=[cam_app, key_app]) 244 | server_thread.start() 245 | 246 | # or you can use a custom handler, 247 | # in which case recv will fail with EINTR 248 | print("registering sigint") 249 | signal.signal(signal.SIGINT, signal_handler) 250 | 251 | try: 252 | print("gst_thread_join") 253 | gst_thread.join() 254 | print("Pausing so that thread doesn't exit") 255 | while(1): 256 | time.sleep(1) 257 | 258 | except: 259 | print("exiting") 260 | exit(0) 261 | --------------------------------------------------------------------------------