├── 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 |
--------------------------------------------------------------------------------