Initial version

This commit is contained in:
Kumi 2022-02-20 17:21:11 +01:00
commit 8407e980cb
12 changed files with 165 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv/
*.pyc
__pycache__/
settings.ini

0
__main__.py Normal file
View file

10
camstream.py Normal file
View file

@ -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()

0
classes/__init__.py Normal file
View file

18
classes/config.py Normal file
View file

@ -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

65
classes/handler.py Normal file
View file

@ -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

32
classes/image.py Normal file
View file

@ -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)

14
classes/server.py Normal file
View file

@ -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()

BIN
img/offline.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
Pillow

5
settings.dist.ini Normal file
View file

@ -0,0 +1,5 @@
[CAMSTREAM]
Source = https://example.com/source
Frequency = 5
Fallback = img/offline.jpeg
Port = 8090

16
static.py Normal file
View file

@ -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': '*'
}