diff --git a/README.md b/README.md index 2ec7e82..b4ae73c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The script is designed to be simple to use with minimal configuration. All video - **-p \**: Port that the stream will be relayed on (default is 54321) - **-w \**: Port that the stream will be relayed on via WebSockets (default is 54322) +- **-f \**: Limit output framerate (default: unlimited) - **-q**: Silence non-essential output - **-d**: Turn debugging on - **stream-source-url**: URL of the existing MJPEG stream. If the stream is protected with HTTP authentication, supply the credentials via the URL like so: `http://user:password@ip:port/path/to/stream/` diff --git a/app/broadcaster.py b/app/broadcaster.py index d2e018d..b31583a 100644 --- a/app/broadcaster.py +++ b/app/broadcaster.py @@ -4,6 +4,7 @@ import requests import re import time +import datetime import base64 from status import Status @@ -28,8 +29,11 @@ class Broadcaster: _instance = None - def __init__(self, url): + def __init__(self, url, framerate): self.url = url + self.framerate = framerate + + logging.info("Desired max. framerate: {}".format(self.framerate)); self.clients = [] self.webSocketClients = [] @@ -41,6 +45,7 @@ def __init__(self, url): self.broadcastThread.daemon = True self.lastFrame = "" + self.lastFrameTime = datetime.datetime.now() self.lastFrameBuffer = "" self.connected = False @@ -154,14 +159,24 @@ def broadcast(self, data): #delete the frame now that it has been extracted, keep what remains in the buffer self.lastFrameBuffer = self.lastFrameBuffer[bufferProcessedTo:] - #save for /snapshot requests - self.lastFrame = frame + self.status.incFramesIn() + #only transmit frames often enough to match framerate + time_delta = datetime.datetime.now() - self.lastFrameTime + if (self.framerate == -1 or time_delta.total_seconds() >= 1.0 / float(self.framerate)): + #save for /snapshot requests + self.lastFrame = frame + + self.lastFrameTime = datetime.datetime.now() + + #serve to websocket clients + self.broadcastToStreamingClients(self.webSocketClients, webSocketFrame) + self.status.addToBytesOut(len(webSocketFrame)*len(self.webSocketClients)) - #serve to websocket clients - self.broadcastToStreamingClients(self.webSocketClients, webSocketFrame) + #serve to standard clients + self.broadcastToStreamingClients(self.clients, mjpegFrame) + self.status.addToBytesOut(len(mjpegFrame)*len(self.clients)) - #serve to standard clients - self.broadcastToStreamingClients(self.clients, mjpegFrame) + self.status.incFramesOut() # # Thread to handle reading the source of the stream and rebroadcasting @@ -176,7 +191,6 @@ def streamFromSource(self): return self.broadcast(data) self.status.addToBytesIn(len(data)) - self.status.addToBytesOut(len(data)*self.getClientCount()) except Exception, e: logging.error("Lost connection to the stream source: {}".format(e)) finally: @@ -188,4 +202,4 @@ def streamFromSource(self): data = self.boundarySeparator + "\r\n" + self.feedLostFrame + "\r\n" self.broadcast(data) time.sleep(5) - self.connectToStream() \ No newline at end of file + self.connectToStream() diff --git a/app/httprequesthandler.py b/app/httprequesthandler.py index 2b6237c..94b12aa 100644 --- a/app/httprequesthandler.py +++ b/app/httprequesthandler.py @@ -68,7 +68,7 @@ def handleRequest(self, clientsock): #explicitly deal with individual requests. Verbose, but more secure if ("/status" in requestPath): clientsock.sendall('HTTP/1.0 200 OK\r\nContentType: text/html\r\n\r\n') - clientsock.sendall(self.statusHTML.format(clientcount = self.broadcast.getClientCount(), bwin = float(self.status.bandwidthIn*8)/1000000, bwout = float(self.status.bandwidthOut*8)/1000000)) + clientsock.sendall(self.statusHTML.format(clientcount = self.broadcast.getClientCount(), bwin = float(self.status.bandwidthIn*8)/1000000, bwout = float(self.status.bandwidthOut*8)/1000000, frin=float(self.status.framerateIn), frout=float(self.status.framerateOut))) clientsock.close() elif ("/style.css" in requestPath): clientsock.sendall('HTTP/1.0 200 OK\r\nContentType: text/html\r\n\r\n') @@ -105,4 +105,4 @@ def acceptClients(self): clientsock.close() return handlethread = threading.Thread(target = self.handleRequest, args = (clientsock,)) - handlethread.start() \ No newline at end of file + handlethread.start() diff --git a/app/status.py b/app/status.py index 3d6ed3b..06c611e 100644 --- a/app/status.py +++ b/app/status.py @@ -7,9 +7,13 @@ class Status: def __init__(self): self.bytesOut = 0 self.bytesIn = 0 + self.framesIn = 0 + self.framesOut = 0 self.bandwidthOut = 0 self.bandwidthIn = 0 + self.framerateIn = 0 + self.framerateOut = 0 Status._instance = self @@ -19,12 +23,22 @@ def addToBytesOut(self, byteCount): def addToBytesIn(self, byteCount): self.bytesIn += byteCount + def incFramesIn(self): + self.framesIn += 1.0 + + def incFramesOut(self): + self.framesOut += 1.0 + def run(self): while True: - self.bandwidthOut = self.bytesOut - self.bandwidthIn = self.bytesIn + self.bandwidthOut = self.bytesOut / 5 + self.bandwidthIn = self.bytesIn / 5 + self.framerateIn = self.framesIn / 5 + self.framerateOut = self.framesOut / 5 self.bytesIn = 0 self.bytesOut = 0 + self.framesIn = 0 + self.framesOut = 0 - time.sleep(1) + time.sleep(5) diff --git a/app/web/status.html b/app/web/status.html index f958f6b..9cea113 100644 --- a/app/web/status.html +++ b/app/web/status.html @@ -19,7 +19,13 @@

Summary

Outgoing bandwidth{bwout:.4f}Mb/s + + Incoming framerate{frin:.1f} fps + + + Outgoing framerate{frout:.1f} fps + - \ No newline at end of file + diff --git a/relay.py b/relay.py index 02c0778..6d74703 100644 --- a/relay.py +++ b/relay.py @@ -36,6 +36,7 @@ def quit(): op.add_option("-p", "--port", action="store", default = 54321, dest="port", help = "Port to serve the MJPEG stream on") op.add_option("-w", "--ws-port", action="store", default = 54322, dest="wsport", help = "Port to serve the MJPEG stream on via WebSockets") + op.add_option("-f", "--framerate", action="store", default = -1, dest="framerate", help = "Maximum framerate to stream at") op.add_option("-q", "--quiet", action="store_true", default = False, dest="quiet", help = "Silence non-essential output") op.add_option("-d", "--debug", action="store_true", default = False, dest="debug", help = "Turn debugging on") @@ -62,12 +63,19 @@ def quit(): op.print_help() sys.exit(1) + try: + options.framerate = int(options.framerate) + except ValueError: + logging.error("Framerate must be numeric") + op.print_help() + sys.exit(1) + Status() statusThread = threading.Thread(target=Status._instance.run) statusThread.daemon = True statusThread.start() - broadcaster = Broadcaster(args[0]) + broadcaster = Broadcaster(args[0], options.framerate) broadcaster.start() requestHandler = HTTPRequestHandler(options.port)