commit 8407e980cb857e052091612ea759fa850dc8ae0d Author: Kumi Date: Sun Feb 20 17:21:11 2022 +0100 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c8f647 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +*.pyc +__pycache__/ +settings.ini \ No newline at end of file diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..e69de29 diff --git a/camstream.py b/camstream.py new file mode 100644 index 0000000..de86b61 --- /dev/null +++ b/camstream.py @@ -0,0 +1,10 @@ +from classes.config import Config +from classes.server import ImageServer + +def main(): + cfg = Config.fromFile("settings.ini") + server = ImageServer(cfg.source, cfg.fallback) + server.start() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/classes/__init__.py b/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/classes/config.py b/classes/config.py new file mode 100644 index 0000000..0b35c87 --- /dev/null +++ b/classes/config.py @@ -0,0 +1,18 @@ +from configparser import ConfigParser + +from static import CONFIG_SECTION, CONFIG_FALLBACK, CONFIG_FREQUENCY, CONFIG_SOURCE, CONFIG_PORT + +class Config: + @classmethod + def fromFile(cls, path): + obj = cls() + parser = ConfigParser() + + parser.read(path) + + obj.source = parser.get(CONFIG_SECTION, CONFIG_SOURCE) + obj.frequency = parser.getint(CONFIG_SECTION, CONFIG_FREQUENCY) + obj.fallback = parser.get(CONFIG_SECTION, CONFIG_FALLBACK) + obj.port = parser.get(CONFIG_SECTION, CONFIG_PORT) + + return obj \ No newline at end of file diff --git a/classes/handler.py b/classes/handler.py new file mode 100644 index 0000000..7e26ade --- /dev/null +++ b/classes/handler.py @@ -0,0 +1,65 @@ +from http.server import BaseHTTPRequestHandler +from urllib.request import urlopen +from urllib.error import URLError +from socket import error, timeout +from io import BytesIO +from time import sleep + +from classes.image import Image +from static import START_HEADERS, PART_BOUNDARY + +class ImageHandler(BaseHTTPRequestHandler): + def __init__(self, source, fallback, *args, **kwargs): + self.source_image = source + self.fallback_image = fallback + super().__init__(*args, **kwargs) + + def get_next_image(self): + try: + bfr = BytesIO() + src = urlopen(self.source_image).read() + bfr.write(src) + bfr.seek(0) + img = Image.open(bfr) + except: + with open(self.fallback_image, "rb") as infile: + bfr = BytesIO() + src = infile.read() + bfr.write(src) + bfr.seek(0) + img = Image.open(bfr) + + return img + + def send_image(self, image=None): + image = image or self.get_next_image() + + self.end_headers() + self.wfile.write(PART_BOUNDARY.encode("utf-8")) + self.end_headers() + + data, headers = image.prepare_sending() + + for key, value in headers.items(): + self.send_header(key, value) + + self.end_headers() + + self.wfile.write(data) + + def do_GET(self): + if not self.path == "/": + self.send_response_only(404) + return + + self.send_response(200) + + for key, value in START_HEADERS.items(): + self.send_header(key, value) + + try: + while True: + self.send_image() + sleep(5) + except: + pass \ No newline at end of file diff --git a/classes/image.py b/classes/image.py new file mode 100644 index 0000000..34ccd62 --- /dev/null +++ b/classes/image.py @@ -0,0 +1,32 @@ +from PIL.Image import open as PILopen +from io import BytesIO + +import time + +class Image: + @classmethod + def open(cls, *args, **kwargs): + img = PILopen(*args, **kwargs) + return cls(img) + + def __init__(self, img): + self._img = img + + def prepare_sending(self): + buffer = BytesIO() + + self.save(buffer, "JPEG") + + headers = { + 'X-Timestamp': time.time(), + 'Content-Length': len(buffer.getvalue()), + 'Content-Type': "image/jpeg" + } + + return buffer.getvalue(), headers + + def __getattr__(self, key): + if key == '_img': + raise AttributeError() + + return getattr(self._img, key) diff --git a/classes/server.py b/classes/server.py new file mode 100644 index 0000000..b84a276 --- /dev/null +++ b/classes/server.py @@ -0,0 +1,14 @@ +from http.server import ThreadingHTTPServer + +from classes.handler import ImageHandler + +class ImageServer: + def __init__(self, source, fallback, port=8090, ip="0.0.0.0"): + class Handler(ImageHandler): + def __init__(self, *args, **kwargs): + super().__init__(source, fallback, *args, **kwargs) + + self.server = ThreadingHTTPServer((ip, port), Handler) + + def start(self): + self.server.serve_forever() \ No newline at end of file diff --git a/img/offline.jpeg b/img/offline.jpeg new file mode 100644 index 0000000..5a44109 Binary files /dev/null and b/img/offline.jpeg differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5873a22 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pillow \ No newline at end of file diff --git a/settings.dist.ini b/settings.dist.ini new file mode 100644 index 0000000..1167c0f --- /dev/null +++ b/settings.dist.ini @@ -0,0 +1,5 @@ +[CAMSTREAM] +Source = https://example.com/source +Frequency = 5 +Fallback = img/offline.jpeg +Port = 8090 \ No newline at end of file diff --git a/static.py b/static.py new file mode 100644 index 0000000..cdc3261 --- /dev/null +++ b/static.py @@ -0,0 +1,16 @@ +PART_BOUNDARY = "--BOUNDARY" + +CONFIG_SECTION = "CAMSTREAM" +CONFIG_SOURCE = "Source" +CONFIG_FREQUENCY = "Frequency" +CONFIG_FALLBACK = "Fallback" +CONFIG_PORT = "Port" + +START_HEADERS = { + 'Cache-Control': 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0', + 'Connection': 'close', + 'Content-Type': 'multipart/x-mixed-replace;boundary=%s' % PART_BOUNDARY, + 'Expires': 'Mon, 1 Jan 2001 00:00:00 GMT', + 'Pragma': 'no-cache', + 'Access-Control-Allow-Origin': '*' + } \ No newline at end of file