Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The script is designed to be simple to use with minimal configuration. All video

- **-p \<relay port\>**: Port that the stream will be relayed on (default is 54321)
- **-w \<WebSocket port\>**: Port that the stream will be relayed on via WebSockets (default is 54322)
- **-f \<framerate\>**: 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/`
Expand Down
32 changes: 23 additions & 9 deletions app/broadcaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import requests
import re
import time
import datetime
import base64
from status import Status

Expand All @@ -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 = []
Expand All @@ -41,6 +45,7 @@ def __init__(self, url):
self.broadcastThread.daemon = True

self.lastFrame = ""
self.lastFrameTime = datetime.datetime.now()
self.lastFrameBuffer = ""

self.connected = False
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -188,4 +202,4 @@ def streamFromSource(self):
data = self.boundarySeparator + "\r\n" + self.feedLostFrame + "\r\n"
self.broadcast(data)
time.sleep(5)
self.connectToStream()
self.connectToStream()
4 changes: 2 additions & 2 deletions app/httprequesthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -105,4 +105,4 @@ def acceptClients(self):
clientsock.close()
return
handlethread = threading.Thread(target = self.handleRequest, args = (clientsock,))
handlethread.start()
handlethread.start()
20 changes: 17 additions & 3 deletions app/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
8 changes: 7 additions & 1 deletion app/web/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ <h2>Summary</h2>
<tr>
<td>Outgoing bandwidth</td><td>{bwout:.4f}Mb/s</td>
</tr>
<tr>
<td>Incoming framerate</td><td>{frin:.1f} fps</td>
</tr>
<tr>
<td>Outgoing framerate</td><td>{frout:.1f} fps</td>
</tr>
</tbody>
</table>
</body>
</html>
</html>
10 changes: 9 additions & 1 deletion relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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)
Expand Down