forked from dmitri-mcguckin/webcamd
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathwebcam.py
executable file
·418 lines (344 loc) · 14.3 KB
/
webcam.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#! /usr/bin/python
# webcamd - A High Performance MJPEG HTTP Server
# Original author: Igor Maculan <[email protected]>
#
# Fixes by Christopher RYU <[email protected]>
# Major refactor and threading optimizations by Shell Shrader <[email protected]>
import os
import sys
import time
import datetime
import signal
import threading
import socket
import cv2
import argparse
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
from PIL import ImageFont, ImageDraw, Image
from io import BytesIO
exitCode = os.EX_OK
myargs = None
webserver = None
lastImage = None
encoderLock = None
encodeFps = 0.
streamFps = {}
snapshots = 0
class WebRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
global exitCode
global myargs
global streamFps
global snapshots
if self.path.lower().startswith("/?snapshot"):
snapshots = snapshots + 1
qs = parse_qs(urlparse(self.path).query)
if "rotate" in qs:
self.sendSnapshot(rotate=int(qs["rotate"][0]))
return
if myargs.rotate != -1:
self.sendSnapshot(rotate=myargs.rotate)
return
self.sendSnapshot()
return
if self.path.lower().startswith("/?stream"):
qs = parse_qs(urlparse(self.path).query)
if "rotate" in qs:
self.streamVideo(rotate=int(qs["rotate"][0]))
return
if myargs.rotate != -1:
self.streamVideo(rotate=myargs.rotate)
return
self.streamVideo()
return
if self.path.lower().startswith("/?info"):
self.send_response(200)
self.send_header("Content-type", "text/json")
self.end_headers()
host = self.headers.get('Host')
fpssum = 0.
fpsavg = 0.
for fps in streamFps:
fpssum = fpssum + streamFps[fps]
if len(streamFps) > 0:
fpsavg = fpssum / len(streamFps)
else:
fpsavg = 0.
jsonstr = ('{"stats":{"server": "%s", "encodeFps": %.2f, "sessionCount": %d, "avgStreamFps": %.2f, "sessions": %s, "snapshots": %d}, "config": %s}' % (host, self.server.getEncodeFps(), len(streamFps), fpsavg, json.dumps(streamFps) if len(streamFps) > 0 else "{}", snapshots, json.dumps(vars(myargs))))
self.wfile.write(jsonstr.encode("utf-8"))
return
if self.path.lower().startswith("/?shutdown"):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
client = ("%s:%d" % (self.client_address[0], self.client_address[1]))
print("%s: shutdown requested by %s" % (datetime.datetime.now(), client), flush=True)
exitCode = os.EX_TEMPFAIL
self.server.shutdown()
self.server.unlockEncoder()
return
self.send_response(404)
self.send_header("Content-type", "text/html")
self.end_headers()
host = self.headers.get('Host')
self.wfile.write((
"<html><head><title>webcamd - A High Performance MJPEG HTTP Server</title></head><body>Specify <a href='http://" + host +
"/?stream'>/?stream</a> to stream, <a href='http://" + host +
"/?snapshot'>/?snapshot</a> for a picture, or <a href='http://" + host +
"/?info'>/?info</a> for statistics and configuration information</body></html>").encode("utf-8"))
def log_message(self, format, *args):
global myargs
if not myargs.loghttp: return
print(("%s: " % datetime.datetime.now()) + (format % args), flush=True)
def streamVideo(self, rotate=-1, showFps = False):
global myargs
global streamFps
frames = 0
self.server.addSession()
streamKey = ("%s:%d" % (socket.getnameinfo((self.client_address[0], 0), 0)[0], self.client_address[1]))
try:
self.send_response(200)
self.send_header(
"Content-type", "multipart/x-mixed-replace; boundary=boundarydonotcross"
)
self.end_headers()
except Exception as e:
print("%s: error in stream header %s: [%s]" % (datetime.datetime.now(), streamKey, e), flush=True)
return
fpsFont = ImageFont.truetype('/home/pi/lcdstats/source-code-pro/SourceCodePro-Regular.ttf', 20)
fpsW, fpsH = fpsFont.getsize("A")
startTime = time.time()
primed = False
addBreaks = False
while self.server.isRunning():
if time.time() > startTime + 5:
streamFps[streamKey] = frames / 5.
# if myargs.showfps: print("%s: streaming @ %.2f FPS to %s - wait time %.5f" % (datetime.datetime.now(), streamFps[streamKey], streamKey, myargs.streamwait), flush=True)
frames = 0
startTime = time.time()
primed = True
jpg = self.server.getImage()
if rotate != -1: jpg = jpg.rotate(rotate)
if myargs.showfps and primed:
draw = ImageDraw.Draw(jpg)
draw.text((0, 0), "%s" % streamKey, font=fpsFont)
draw.text((0, fpsH + 1), "%s" % datetime.datetime.now(), font=fpsFont)
draw.text((0, fpsH * 2 + 2), "Encode: %.0f FPS" % self.server.getEncodeFps(), font=fpsFont)
if streamKey in streamFps:
fpssum = 0.
fpsavg = 0.
for fps in streamFps:
fpssum = fpssum + streamFps[fps]
fpsavg = fpssum / len(streamFps)
draw.text((0, fpsH * 3 + 3), "Streams: %d @ %.1f FPS (avg)" % (len(streamFps), streamFps[streamKey]), font=fpsFont)
try:
tmpFile = BytesIO()
jpg.save(tmpFile, format="JPEG")
if not addBreaks:
self.wfile.write(b"--boundarydonotcross\r\n")
addBreaks = True
else:
self.wfile.write(b"\r\n--boundarydonotcross\r\n")
self.send_header("Content-type", "image/jpeg")
self.send_header("Content-length", str(tmpFile.getbuffer().nbytes))
self.send_header("X-Timestamp", "0.000000")
self.end_headers()
self.wfile.write(tmpFile.getvalue())
time.sleep(myargs.streamwait)
frames = frames + 1
except Exception as e:
# ignore broken pipes & connection reset
if e.args[0] not in (32, 104): print("%s: error in stream %s: [%s]" % (datetime.datetime.now(), streamKey, e), flush=True)
break
if streamKey in streamFps: streamFps.pop(streamKey)
self.server.dropSession()
def sendSnapshot(self, rotate=-1):
global lastImage
self.server.addSession()
try:
self.send_response(200)
jpg = self.server.getImage()
if rotate != -1: jpg = jpg.rotate(rotate)
fpsFont = ImageFont.truetype('/home/pi/lcdstats/source-code-pro/SourceCodePro-Regular.ttf', 20)
fpsW, fpsH = fpsFont.getsize("A")
draw = ImageDraw.Draw(jpg)
draw.text((0, 0), "%s" % socket.getnameinfo((self.client_address[0], 0), 0)[0], font=fpsFont)
draw.text((0, fpsH + 1), "%s" % datetime.datetime.now(), font=fpsFont)
tmpFile = BytesIO()
jpg.save(tmpFile, "JPEG")
self.send_header("Content-type", "image/jpeg")
self.send_header("Content-length", str(len(tmpFile.getvalue())))
self.end_headers()
self.wfile.write(tmpFile.getvalue())
except Exception as e:
print("%s: error in snapshot: [%s]" % (datetime.datetime.now(), e), flush=True)
self.server.dropSession()
def web_server_thread():
global exitCode
global myargs
global webserver
global encoderLock
global encodeFps
try:
if myargs.ipv == 4:
webserver = ThreadingHTTPServer((myargs.v4bindaddress, myargs.port), WebRequestHandler)
else:
webserver = ThreadingHTTPServerV6((myargs.v6bindaddress, myargs.port), WebRequestHandler)
print("%s: web server started" % datetime.datetime.now(), flush=True)
webserver.serve_forever()
except Exception as e:
exitCode = os.EX_SOFTWARE
print("%s: web server error: [%s]" % (datetime.datetime.now(), e), flush=True)
print("%s: web server thread dead" % (datetime.datetime.now()), flush=True)
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
running = True
sessions = 0
def __init__(self, mixin, server):
global encoderLock
encoderLock.acquire()
super().__init__(mixin, server)
def getImage(self):
global lastImage
return lastImage
def shutdown(self):
super().shutdown()
self.running = False
def isRunning(self):
return self.running
def addSession(self):
global encoderLock
if self.sessions == 0 and encoderLock.locked(): encoderLock.release()
self.sessions = self.sessions + 1
def dropSession(self):
global encoderLock
global encodeFps
global streamFps
self.sessions = self.sessions - 1
if self.sessions == 0 and not encoderLock.locked():
encoderLock.acquire()
encodeFps = 0.
streamFps = {}
def unlockEncoder(self):
global encoderLock
if encoderLock.locked(): encoderLock.release()
def getSessions(self):
return self.sessions
def getEncodeFps(self):
global encodeFps
return encodeFps
class ThreadingHTTPServerV6(ThreadingHTTPServer):
address_family = socket.AF_INET6
def main():
global exitCode
global myargs
global webserver
global lastImage
global encoderLock
global encodeFps
signal.signal(signal.SIGTERM, exit_gracefully)
# set_start_method('fork')
parseArgs()
encoderLock = threading.Lock()
threading.Thread(target=web_server_thread).start()
# Process(target=web_server_thread).start()
# wait for our webserver to start
while webserver is None and exitCode == os.EX_OK:
time.sleep(.01)
# initialize our opencv encoder
capture = cv2.VideoCapture(myargs.index)
capture.set(cv2.CAP_PROP_FRAME_WIDTH, myargs.width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, myargs.height)
frames = 0
startTime = time.time()
while not webserver is None and webserver.isRunning():
if time.time() > startTime + 5:
encodeFps = frames / 5.
# if myargs.showfps: print("%s: encoding @ %.2f FPS - wait time %.5f" % (datetime.datetime.now(), encodeFps, myargs.encodewait), flush=True)
frames = 0
startTime = time.time()
try:
rc, img_bgr = capture.read()
if not rc:
print("%s: restarting encoder due to timeouts" % datetime.datetime.now(), flush=True)
capture.release()
time.sleep(myargs.encodewait)
capture = cv2.VideoCapture(myargs.index)
capture.set(cv2.CAP_PROP_FRAME_WIDTH, myargs.width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, myargs.height)
continue
# img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
# if myargs.rotate != -1: img = cv2.rotate(img, myargs.rotate)
# img = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
# if myargs.rotate != -1: img = img.rotate(myargs.rotate)
lastImage = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
time.sleep(myargs.encodewait)
frames = frames + 1.0
if encoderLock.locked():
encoderLock.acquire()
encoderLock.release()
except KeyboardInterrupt:
break
except Exception as e:
exitCode = os.EX_SOFTWARE
print("%s: error in capture: [%s]" % (datetime.datetime.now(), e), flush=True)
break
if not webserver is None and webserver.isRunning():
print("%s: web server shutting down" % (datetime.datetime.now()), flush=True)
webserver.shutdown()
print("%s: ExitCode=%d - Goodbye!" % (datetime.datetime.now(), exitCode), flush=True)
sys.exit(exitCode)
def parseArgs():
global myargs
parser = argparse.ArgumentParser(
description="webcam.py - A High Performance MJPEG HTTP Server"
)
parser.add_argument(
"--width",
type=int,
default=1280,
help="Web camera pixel width (default 1280)"
)
parser.add_argument(
"--height",
type=int,
default=720,
help="Web camera pixel height (default 720)",
)
parser.add_argument(
"--index", type=int, default=0, help="Video device to stream /dev/video# (default #=0)"
)
parser.add_argument("--ipv", type=int, default=4, help="IP version (default=4)")
parser.add_argument(
"--v4bindaddress",
type=str,
default="0.0.0.0",
help="IPv4 HTTP bind address (default '0.0.0.0')",
)
parser.add_argument(
"--v6bindaddress",
type=str,
default="::",
help="IPv6 HTTP bind address (default '::')",
)
parser.add_argument(
"--port", type=int, default=8080, help="HTTP bind port (default 8080)"
)
parser.add_argument(
"--encodewait", type=float, default=.01, help="seconds to pause between encoding frames (default .01)"
)
parser.add_argument(
"--streamwait", type=float, default=.01, help="seconds to pause between streaming frames (default .01)"
)
parser.add_argument(
"--rotate", type=int, default=-1, help="rotate captured image 1-359 in degrees - (default no rotation)"
)
parser.add_argument('--showfps', action='store_true', help="periodically show encoding / streaming frame rate (default false)")
parser.add_argument('--loghttp', action='store_true', help="enable http server logging (default false)")
myargs = parser.parse_args()
def exit_gracefully(signum, frame):
raise KeyboardInterrupt()
if __name__ == "__main__":
main()