Initial version
This commit is contained in:
commit
8407e980cb
12 changed files with 165 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
venv/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
settings.ini
|
0
__main__.py
Normal file
0
__main__.py
Normal file
10
camstream.py
Normal file
10
camstream.py
Normal 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
0
classes/__init__.py
Normal file
18
classes/config.py
Normal file
18
classes/config.py
Normal 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
65
classes/handler.py
Normal 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
32
classes/image.py
Normal 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
14
classes/server.py
Normal 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
BIN
img/offline.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 234 KiB |
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Pillow
|
5
settings.dist.ini
Normal file
5
settings.dist.ini
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[CAMSTREAM]
|
||||||
|
Source = https://example.com/source
|
||||||
|
Frequency = 5
|
||||||
|
Fallback = img/offline.jpeg
|
||||||
|
Port = 8090
|
16
static.py
Normal file
16
static.py
Normal 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': '*'
|
||||||
|
}
|
Loading…
Reference in a new issue