initial checkin

This commit is contained in:
moo
2025-12-22 06:32:34 +01:00
commit 3446b2621f
18 changed files with 628 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
#REPOSITORY=registry.dissertori.lan
#REPOSITORY=10.0.1.52:5000
REPOSITORY=moothecow
#REPOSITORY=172.105.85.44
VERSION=0.0.14
+28
View File
@@ -0,0 +1,28 @@
FROM debian:bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
meson ninja-build git build-essential ca-certificates \
cmake \
&& rm -rf /var/lib/apt/lists/*
#RUN pip3 install --break-system-packages meson ninja
RUN git clone https://github.com/moo-the-cow/librist && \
cd librist && \
mkdir build && \
cd build && \
meson .. --default-library=static --buildtype=release -Db_lto=true && \
ninja
FROM busybox:glibc
#COPY --from=builder /librist/build/tools/rist2rist /usr/bin/
COPY --from=builder /librist/build/tools/ristreceiver /usr/bin/
#COPY --from=builder /librist/build/tools/ristsender /usr/bin/
#COPY --from=builder /librist/build/tools/ristsrppasswd /usr/bin/
COPY banner.txt /
# Create the entrypoint.sh script directly in the Dockerfile
RUN echo '#!/bin/sh' > /entrypoint.sh \
&& echo 'cat /banner.txt' >> /entrypoint.sh \
&& echo 'exec "$@"' >> /entrypoint.sh \
&& chmod +x /entrypoint.sh
# Set the entrypoint to the created script
ENTRYPOINT ["/entrypoint.sh"]
+5
View File
@@ -0,0 +1,5 @@
#FROM python:3.11.4-slim-bookworm
FROM python:3.11.11-alpine3.21
COPY ./UdpLogReceiver.py ./banner.txt /opt/moostream/
WORKDIR /opt/moostream
ENTRYPOINT ["python3","UdpLogReceiver.py"]
+4
View File
@@ -0,0 +1,4 @@
FROM python:3.11.11-alpine3.21
COPY ./UdpLogSender.py ./banner.txt /opt/moostream/
WORKDIR /opt/moostream
ENTRYPOINT ["python3","UdpLogSender.py"]
+6
View File
@@ -0,0 +1,6 @@
FROM python:3.11.11-alpine3.21
RUN pip install websockets
COPY ./StatsServer.py ./banner.txt /opt/moostream/
COPY ./logfile.json ./banner.txt /opt/moostream/
WORKDIR /opt/moostream
ENTRYPOINT ["python3","StatsServer.py"]
+5
View File
@@ -0,0 +1,5 @@
FROM python:3.11.11-alpine3.21
RUN pip install websockets
COPY ./StatsServerSender.py ./banner.txt /opt/moostream/
WORKDIR /opt/moostream
ENTRYPOINT ["python3","StatsServerSender.py"]
+28
View File
@@ -0,0 +1,28 @@
FROM debian:bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
meson ninja-build git build-essential ca-certificates \
cmake \
&& rm -rf /var/lib/apt/lists/*
#RUN pip3 install --break-system-packages meson ninja
RUN git clone https://github.com/moo-the-cow/librist && \
cd librist && \
mkdir build && \
cd build && \
meson .. --default-library=static --buildtype=release -Db_lto=true && \
ninja
FROM busybox:glibc
#COPY --from=builder /librist/build/tools/rist2rist /usr/bin/
#COPY --from=builder /librist/build/tools/ristreceiver /usr/bin/
COPY --from=builder /librist/build/tools/ristsender /usr/bin/
#COPY --from=builder /librist/build/tools/ristsrppasswd /usr/bin/
COPY banner.txt /
# Create the entrypoint.sh script directly in the Dockerfile
RUN echo '#!/bin/sh' > /entrypoint.sh \
&& echo 'cat /banner.txt' >> /entrypoint.sh \
&& echo 'exec "$@"' >> /entrypoint.sh \
&& chmod +x /entrypoint.sh
# Set the entrypoint to the created script
ENTRYPOINT ["/entrypoint.sh"]
+28
View File
@@ -0,0 +1,28 @@
FROM debian:bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
meson ninja-build git build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*
#RUN pip3 install --break-system-packages meson ninja
RUN GIT_SSL_NO_VERIFY=true git clone https://github.com/moo-the-cow/librist && \
cd librist && \
mkdir build && \
cd build && \
meson .. --default-library=static --buildtype=release -Db_lto=true && \
ninja
FROM busybox:glibc
COPY --from=builder /librist/build/tools/rist2rist /usr/bin/
#COPY --from=builder /librist/build/tools/ristreceiver /usr/bin/
#COPY --from=builder /librist/build/tools/ristsender /usr/bin/
#COPY --from=builder /librist/build/tools/ristsrppasswd /usr/bin/
COPY banner.txt /
# Create the entrypoint.sh script directly in the Dockerfile
RUN echo '#!/bin/sh' > /entrypoint.sh \
&& echo 'cat /banner.txt' >> /entrypoint.sh \
&& echo 'exec "$@"' >> /entrypoint.sh \
&& chmod +x /entrypoint.sh
# Set the entrypoint to the created script
ENTRYPOINT ["/entrypoint.sh"]
+181
View File
@@ -0,0 +1,181 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
from pathlib import Path
import asyncio
import websockets
import threading
import sys
from websockets.exceptions import ConnectionClosed, InvalidHandshake, InvalidMessage
import socket
import json
#UDP BEGIN
UDP_ADDR = os.getenv('env_udp_addr', '0.0.0.0')
UDP_PORT = int(os.getenv('env_udp_port', '5005'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
def checklogs():
print(f'Starting UDP log check udp://{UDP_ADDR}:{UDP_PORT}...\n')
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_ADDR, UDP_PORT))
try:
while True:
data, addr = sock.recvfrom(32 * 1024) # buffer size
decoded_data = data.decode("utf-8")
parts = decoded_data.split('|')
# Check if the log entry has the expected structure
if len(parts) < 3:
continue
# Extract the relevant part
relevant_data = parts[2].strip().replace("[INFO] ", "")
if "receiver-stats" in relevant_data:
try:
# Attempt to parse as JSON
json_data = json.loads(relevant_data)
with open(LOGFILE_PATH, 'w') as f:
json.dump(json_data, f)
f.flush() # Force write to disk
os.fsync(f.fileno()) # Ensure it's written to filesystem
except json.JSONDecodeError:
print(f"Skipping invalid JSON: {relevant_data}")
elif "[CLEANUP]" in relevant_data:
print("Cleanup log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
f.flush()
os.fsync(f.fileno())
elif "has timed out" in relevant_data:
print("Disconnect log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
f.flush()
os.fsync(f.fileno())
except KeyboardInterrupt:
pass
finally:
sock.close()
print(f"...Stopped UDP log check\n")
#UDP END
HTTP_ADDR = os.getenv('env_http_addr', '0.0.0.0')
HTTP_PORT = int(os.getenv('env_http_port', '8080'))
WS_PORT = int(os.getenv('env_ws_port', '8081'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
json_string = '{"receiver-stats":null}'
async def handler(websocket):
"""WebSocket handler with improved error handling"""
global json_string
while True:
try:
await asyncio.sleep(0.5)
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
if json_string != '{"receiver-stats":null}':
reply = f"{json_string}"
await websocket.send(reply)
except ConnectionClosed:
break # Client disconnected normally
except Exception as e:
print(f"WebSocket error: {str(e)}")
break # Disconnect on other errors
class StatsServer(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
def log_message(self, format, *args):
# Override to prevent logging
return
def do_HEAD(self):
self._set_headers()
def do_GET(self):
try:
self._set_headers()
global json_string
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
byte_json_string_utf8 = json_string.encode('utf-8')
self.wfile.write(byte_json_string_utf8)
except BrokenPipeError:
# Client disconnected before response could be sent
pass
except ConnectionResetError:
# Client reset the connection
pass
except Exception as e:
print(f"HTTP request error: {str(e)}")
self.send_error(500, "Internal Server Error")
def run_http_server():
server_address = (HTTP_ADDR, HTTP_PORT)
httpd = HTTPServer(server_address, StatsServer)
print(f'Starting HTTP server at http://{HTTP_ADDR}:{HTTP_PORT}...')
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
except Exception as e:
print(f"HTTP server error: {str(e)}")
finally:
httpd.server_close()
print('HTTP server stopped.')
async def start_websocket_server():
"""WebSocket server with comprehensive error handling"""
try:
async with websockets.serve(
handler,
HTTP_ADDR,
WS_PORT,
# Additional settings for better compatibility
ping_interval=20,
ping_timeout=20,
close_timeout=10,
) as server:
print(f'WebSocket server started on ws://{HTTP_ADDR}:{WS_PORT}')
await asyncio.Future() # Run forever
except InvalidHandshake as e:
print(f"WebSocket handshake failed: {str(e)}")
except Exception as e:
print(f"WebSocket server error: {str(e)}")
if __name__ == "__main__":
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
f.flush()
os.fsync(f.fileno())
print(Path("banner.txt").read_text())
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
# Start UDP logger
udp_thread = threading.Thread(target=checklogs)
udp_thread.daemon = True
udp_thread.start()
# Start HTTP server in a separate thread
http_thread = threading.Thread(target=run_http_server)
http_thread.daemon = True
http_thread.start()
try:
asyncio.run(start_websocket_server())
except KeyboardInterrupt:
print('\nShutting down servers...')
except Exception as e:
print(f"Main error: {str(e)}")
finally:
sys.exit(0)
+46
View File
@@ -0,0 +1,46 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
from pathlib import Path
HTTP_ADDR = os.getenv('env_http_addr', '0.0.0.0')
HTTP_PORT = int(os.getenv('env_http_port', '8080'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
json_string = '{"receiver-stats":null}'
class StatsServer(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
def do_HEAD(self):
self._set_headers()
def do_GET(self):
self._set_headers()
global json_string
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
byte_json_string_utf8 = json_string.encode('utf-8')
print(byte_json_string_utf8)
self.wfile.write(byte_json_string_utf8)
def runhttpserver(server_class=HTTPServer, handler_class=StatsServer, port=HTTP_PORT):
print(f'Starting httpd http://{HTTP_ADDR}:{HTTP_PORT}...\n')
global json_string
print(json_string)
server_address = (HTTP_ADDR, port)
httpd = server_class(server_address, handler_class)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print(f'... Stopped Httpd Server \n')
if __name__ == "__main__":
print(Path("banner.txt").read_text())
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
runhttpserver()
+78
View File
@@ -0,0 +1,78 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
from pathlib import Path
import asyncio
import websockets
import threading
HTTP_ADDR = os.getenv('env_http_addr', '0.0.0.0')
HTTP_PORT = int(os.getenv('env_http_port', '8080'))
WS_PORT = int(os.getenv('env_ws_port', '8081'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
json_string = '{"sender-stats":null}'
async def handler(websocket, path):
global json_string
while True:
await asyncio.sleep(0.5)
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
if(json_string != '{"sender-stats":null}'):
reply = f"{json_string}"
await websocket.send(reply)
class StatsServer(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
def log_message(self, format, *args):
# Override to prevent logging
return
def do_HEAD(self):
self._set_headers()
def do_GET(self):
self._set_headers()
global json_string
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
byte_json_string_utf8 = json_string.encode('utf-8')
self.wfile.write(byte_json_string_utf8)
def run_http_server():
server_address = (HTTP_ADDR, HTTP_PORT)
httpd = HTTPServer(server_address, StatsServer)
print(f'Starting HTTP server at http://{HTTP_ADDR}:{HTTP_PORT}...')
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print('HTTP server stopped.')
if __name__ == "__main__":
print(Path("banner.txt").read_text())
if Path.exists(Path(LOGFILE_PATH)):
with open(LOGFILE_PATH, 'r') as f:
json_string = f.read()
# Start HTTP server in a separate thread
http_thread = threading.Thread(target=run_http_server)
http_thread.start()
# Setup and run the WebSocket server
async def start_websocket_server():
async with websockets.serve(handler, HTTP_ADDR, WS_PORT):
print(f'WebSocket server started on ws://{HTTP_ADDR}:{WS_PORT}')
await asyncio.Future() # Keep the server running
try:
asyncio.run(start_websocket_server())
except KeyboardInterrupt:
print('WebSocket server stopped.')
+53
View File
@@ -0,0 +1,53 @@
import socket
import os
import json
from pathlib import Path
UDP_ADDR = os.getenv('env_udp_addr', '0.0.0.0')
UDP_PORT = int(os.getenv('env_udp_port', '5005'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
def checklogs():
print(f'Starting UDP log check udp://{UDP_ADDR}:{UDP_PORT}...\n')
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_ADDR, UDP_PORT))
try:
while True:
data, addr = sock.recvfrom(32 * 1024) # buffer size
decoded_data = data.decode("utf-8")
parts = decoded_data.split('|')
# Check if the log entry has the expected structure
if len(parts) < 3:
continue
# Extract the relevant part
relevant_data = parts[2].strip().replace("[INFO] ", "")
if "receiver-stats" in relevant_data:
try:
# Attempt to parse as JSON
json_data = json.loads(relevant_data)
with open(LOGFILE_PATH, 'w') as f:
json.dump(json_data, f)
except json.JSONDecodeError:
print(f"Skipping invalid JSON: {relevant_data}")
elif "[CLEANUP]" in relevant_data:
print("Cleanup log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
elif "has timed out" in relevant_data:
print("Disconnect log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
except KeyboardInterrupt:
pass
finally:
sock.close()
print(f"...Stopped UDP log check\n")
if __name__ == "__main__":
print(Path("banner.txt").read_text())
with open(LOGFILE_PATH, 'w') as f:
f.write('{"receiver-stats":null}')
checklogs()
+53
View File
@@ -0,0 +1,53 @@
import socket
import os
import json
from pathlib import Path
UDP_ADDR = os.getenv('env_udp_addr', '0.0.0.0')
UDP_PORT = int(os.getenv('env_udp_port', '5005'))
LOGFILE_PATH = os.getenv('env_logfile_path', 'logfile.json')
def checklogs():
print(f'Starting UDP log check udp://{UDP_ADDR}:{UDP_PORT}...\n')
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_ADDR, UDP_PORT))
try:
while True:
data, addr = sock.recvfrom(32 * 1024) # buffer size
decoded_data = data.decode("utf-8")
parts = decoded_data.split('|')
# Check if the log entry has the expected structure
if len(parts) < 3:
continue
# Extract the relevant part
relevant_data = parts[2].strip().replace("[INFO] ", "")
if "sender-stats" in relevant_data:
try:
# Attempt to parse as JSON
json_data = json.loads(relevant_data)
with open(LOGFILE_PATH, 'w') as f:
json.dump(json_data, f)
except json.JSONDecodeError:
print(f"Skipping invalid JSON: {relevant_data}")
elif "[CLEANUP]" in relevant_data:
print("Cleanup log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"sender-stats":null}')
elif "has timed out" in relevant_data:
print("Disconnect log detected.")
with open(LOGFILE_PATH, 'w') as f:
f.write('{"sender-stats":null}')
except KeyboardInterrupt:
pass
finally:
sock.close()
print(f"...Stopped UDP log check\n")
if __name__ == "__main__":
print(Path("banner.txt").read_text())
with open(LOGFILE_PATH, 'w') as f:
f.write('{"sender-stats":null}')
checklogs()
+12
View File
@@ -0,0 +1,12 @@
==========================================================
Moostream at https://github.com/moo-the-cow/
==========================================================
_______________________________________
/ Streaming for everyone! Cows and even \
\ humans are welcome! /
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
+57
View File
@@ -0,0 +1,57 @@
services:
moo-rist:
build:
context: .
dockerfile: ./Dockerfile
platforms:
- linux/amd64
- linux/arm64
image: ${REPOSITORY}/moo-rist:${VERSION}
moo-rist-forwarder:
build:
context: .
dockerfile: ./DockerfileForwarder
platforms:
- linux/amd64
- linux/arm64
image: ${REPOSITORY}/moo-rist-forwarder:${VERSION}
#moo-rist-relay:
# build:
# context: .
# dockerfile: ./DockerfileRelay
# platforms:
# - linux/amd64
# - linux/arm64
# image: ${REPOSITORY}/moo-rist-relay:${VERSION}
#moostream-logger:
# build:
# context: .
# dockerfile: ./Dockerfile-Logger
# platforms:
# - linux/amd64
# - linux/arm64
# image: ${REPOSITORY}/moo-rist-logger:${VERSION}
#moostream-logger-sender:
# build:
# context: .
# dockerfile: ./Dockerfile-Logger-Sender
# platforms:
# - linux/amd64
# - linux/arm64
# image: ${REPOSITORY}/moo-rist-logger-sender:${VERSION}
moostream-stats:
build:
context: .
dockerfile: ./Dockerfile-Stats
platforms:
- linux/amd64
- linux/arm64
image: ${REPOSITORY}/moo-rist-stats:${VERSION}
#moostream-stats-sender:
# build:
# context: .
# dockerfile: ./Dockerfile-Stats-Sender
# platforms:
# - linux/amd64
# - linux/arm64
# image: ${REPOSITORY}/moo-rist-stats-sender:${VERSION}
+26
View File
@@ -0,0 +1,26 @@
services:
moo-rist:
build:
context: .
dockerfile: ./Dockerfile
image: ${REPOSITORY}/moo-rist:${VERSION}
moostream-logger:
build:
context: .
dockerfile: ./Dockerfile-Logger
image: ${REPOSITORY}/moo-rist-logger:${VERSION}
moostream-logger-sender:
build:
context: .
dockerfile: ./Dockerfile-Logger-Sender
image: ${REPOSITORY}/moo-rist-logger-sender:${VERSION}
moostream-stats:
build:
context: .
dockerfile: ./Dockerfile-Stats
image: ${REPOSITORY}/moo-rist-stats:${VERSION}
moostream-stats-sender:
build:
context: .
dockerfile: ./Dockerfile-Stats-Sender
image: ${REPOSITORY}/moo-rist-stats-sender:${VERSION}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
if [[ -n "$AES_ENC" ]] ; then export ENCRYPTION_PAR = "&aes-type=$AES_ENC" ; else export ENCRYPTION_PAR = '' ; fi
if [[ -n "$RTT_MIN" ]] ; then export RTT_MIN_PAR = "&rtt-min=$RTT_MIN" ; else export RTT_MIN_PAR = '' ; fi
if [[ -n "$RTT_MAX" ]] ; then export RTT_MAX_PAR = "&rtt-max=$RTT_MAX" ; else export RTT_MAX_PAR = '' ; fi
if [[ -n "$BUFFER" ]] ; then export BUFFER_PAR = "&buffer=$BUFFER" ; else export BUFFER_PAR = '' ; fi
if [[ -n "$REORDER_BUFFER" ]] ; then export REORDER_BUFFER_PAR = "&reorder-buffer=$REORDER_BUFFER" ; else export REORDER_BUFFER_PAR = '' ; fi
if [[ -n "$SESSION_TIMEOUT" ]] ; then export SESSION_TIMEOUT_PAR = "&session-timeout=$SESSION_TIMEOUT" ; else export SESSION_TIMEOUT_PAR = '' ; fi
if [[ -n "$RETURN_BANDWIDTH" ]] ; then export RETURN_BANDWIDTH_PAR = "&return-bandwidth=$RETURN_BANDWIDTH" ; else export RETURN_BANDWIDTH_PAR = '' ; fi
if [[ -n "$MIN_RETRIES" ]] ; then export MIN_RETRIES_PAR = "&min-retries=$MIN_RETRIES" ; else export MIN_RETRIES_PAR = '' ; fi
if [[ -n "$MAX_RETRIES" ]] ; then export MAX_RETRIES_PAR = "&max-retries=$MAX_RETRIES" ; else export MAX_RETRIES_PAR = '' ; fi
if [[ -n "$TIMING_MODE" ]] ; then export TIMING_MODE_PAR = "&timing-mode=$TIMING_MODE" ; else export TIMING_MODE_PAR = '' ; fi
echo -e "RELAY ID: $RIST_RELAY_KEY\nOBS HOST IP: $OBS_HOST_IP\nENC: $ENCRYPTION_PAR" && /usr/bin/ristreceiver -i "rist://@0.0.0.0:2030?secret=$RIST_RELAY_KEY$ENCRYPTION_PAR$RTT_MIN_PAR$RTT_MAX_PAR$BUFFER_PAR$REORDER_BUFFER_PAR$SESSION_TIMEOUT_PAR$RETURN_BANDWIDTH_PAR$MIN_RETRIES_PAR$MAX_RETRIES_PAR$TIMING_MODE_PAR" -o "udp://$OBS_HOST_IP:5556" -r "moo-rist-logger:5005" -p 1
+1
View File
@@ -0,0 +1 @@
{"receiver-stats":null}