This commit is contained in:
commit
7510dc8b6f
16
.drone.yml
Normal file
16
.drone.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: kubernetes
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: harbor.k-space.ee/${DRONE_REPO}
|
||||||
|
registry: harbor.k-space.ee
|
||||||
|
mtu: 1300
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM ubuntu
|
||||||
|
WORKDIR /app
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gstreamer1.0-libav \
|
||||||
|
gstreamer1.0-plugins-bad \
|
||||||
|
gstreamer1.0-plugins-base \
|
||||||
|
gstreamer1.0-plugins-good \
|
||||||
|
gstreamer1.0-plugins-ugly \
|
||||||
|
gstreamer1.0-tools \
|
||||||
|
python3-gevent \
|
||||||
|
python3-numpy \
|
||||||
|
python3-opencv \
|
||||||
|
python3-pip \
|
||||||
|
&& apt-get clean
|
||||||
|
RUN pip3 install prometheus_client aiohttp sanic==21.6.2 sanic_prometheus motor kubernetes
|
||||||
|
COPY camtiler.py /app
|
||||||
|
ENTRYPOINT /app/camtiler.py
|
||||||
|
EXPOSE 5001
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
185
camtiler.py
Executable file
185
camtiler.py
Executable file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import cv2
|
||||||
|
import io
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from kubernetes import client, config
|
||||||
|
from math import ceil
|
||||||
|
from prometheus_client import Counter, Gauge
|
||||||
|
from sanic import Sanic, response
|
||||||
|
from sanic.response import stream
|
||||||
|
from sanic_prometheus import monitor
|
||||||
|
|
||||||
|
targets = [(j,j) for j in sys.argv[1:]]
|
||||||
|
if not targets:
|
||||||
|
# If no targets are specified, fall back to Kube API
|
||||||
|
config.load_kube_config()
|
||||||
|
v1 = client.CoreV1Api()
|
||||||
|
for i in v1.list_namespaced_service("camtiler", label_selector="component=camdetect").items:
|
||||||
|
app.ctx.frames[i.metadata.name] = None
|
||||||
|
url = "http://%s:%d/bypass" % (i.metadata.name, i.spec.ports[0].port)
|
||||||
|
targets.append((i.metadata.name, url))
|
||||||
|
|
||||||
|
print("Running with following targets:")
|
||||||
|
for name, url in targets:
|
||||||
|
print(url)
|
||||||
|
|
||||||
|
counter_dropped_bytes = Counter(
|
||||||
|
"camtiler_client_dropped_bytes",
|
||||||
|
"Bytes that were not not handled or part of actual JPEG frames")
|
||||||
|
counter_rx_bytes = Counter(
|
||||||
|
"camtiler_client_rx_bytes",
|
||||||
|
"Bytes received over HTTP stream")
|
||||||
|
counter_tx_bytes = Counter(
|
||||||
|
"camtiler_client_tx_bytes",
|
||||||
|
"Bytes transmitted over HTTP streams")
|
||||||
|
counter_rx_frames = Counter(
|
||||||
|
"camtiler_client_rx_frames",
|
||||||
|
"Frames received")
|
||||||
|
counter_tx_frames = Counter(
|
||||||
|
"camtiler_client_tx_frames",
|
||||||
|
"Frames transmitted")
|
||||||
|
counter_tx_events = Counter(
|
||||||
|
"camtiler_client_tx_events",
|
||||||
|
"Events emitted")
|
||||||
|
counter_eos = Counter(
|
||||||
|
"camtiler_client_eos",
|
||||||
|
"Count of End of Stream occurrences")
|
||||||
|
counter_timeout_errors = Counter(
|
||||||
|
"camtiler_client_timeout_errors",
|
||||||
|
"Upstream connection timeout errors")
|
||||||
|
counter_cancelled_errors = Counter(
|
||||||
|
"camtiler_client_cancelled_errors",
|
||||||
|
"Upstream connection cancelled errors")
|
||||||
|
counter_incomplete_read_errors = Counter(
|
||||||
|
"camtiler_client_incomplete_read_errors",
|
||||||
|
"Upstream incomplete read errors")
|
||||||
|
|
||||||
|
app = Sanic("camtiler")
|
||||||
|
|
||||||
|
STREAM_RESPONSE = \
|
||||||
|
b"""
|
||||||
|
--frame
|
||||||
|
Content-Type: image/jpeg
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
MERGED_SUBSAMPLE=3
|
||||||
|
|
||||||
|
# Consider camera gone after 5sec
|
||||||
|
TIMEOUT = 5.0
|
||||||
|
|
||||||
|
# Blank frame
|
||||||
|
BLANK = np.full((720 // MERGED_SUBSAMPLE, 1280 // MERGED_SUBSAMPLE, 3), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def tilera(iterable, filler):
|
||||||
|
hstacking = ceil(len(iterable) ** 0.5)
|
||||||
|
vstacking = ceil(len(iterable) / hstacking)
|
||||||
|
rows = []
|
||||||
|
for j in range(0, vstacking):
|
||||||
|
row = []
|
||||||
|
first_frame = True
|
||||||
|
for i in range(0, hstacking):
|
||||||
|
try:
|
||||||
|
frame = iterable[i + hstacking * j]
|
||||||
|
except IndexError:
|
||||||
|
frame = filler
|
||||||
|
|
||||||
|
if not first_frame:
|
||||||
|
row.append(np.full((len(frame), 1, 3), 0))
|
||||||
|
first_frame = False
|
||||||
|
row.append(frame)
|
||||||
|
stacked = np.hstack(row)
|
||||||
|
rows.append(stacked)
|
||||||
|
return np.vstack(rows)
|
||||||
|
|
||||||
|
|
||||||
|
async def client_connect(name, resp):
|
||||||
|
buf = b""
|
||||||
|
print("Upstream connection opened with status:", resp.status)
|
||||||
|
async for data, end_of_http_chunk in resp.content.iter_chunks():
|
||||||
|
counter_rx_bytes.inc(len(data))
|
||||||
|
if end_of_http_chunk:
|
||||||
|
counter_eos.inc()
|
||||||
|
break
|
||||||
|
|
||||||
|
if buf:
|
||||||
|
# seek end
|
||||||
|
marker = data.find(b'\xff\xd9')
|
||||||
|
if marker < 0:
|
||||||
|
buf += data
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
blob = np.frombuffer(buf + data[:marker+2], dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(blob, cv2.IMREAD_UNCHANGED)
|
||||||
|
app.ctx.frames[name] = img
|
||||||
|
data = data[marker+2:]
|
||||||
|
buf = b""
|
||||||
|
counter_rx_frames.inc()
|
||||||
|
|
||||||
|
# seek begin
|
||||||
|
marker = data.find(b'\xff\xd8')
|
||||||
|
if marker >= 0:
|
||||||
|
buf = data[marker:]
|
||||||
|
else:
|
||||||
|
counter_dropped_bytes.inc(len(data))
|
||||||
|
|
||||||
|
|
||||||
|
async def client(name, url):
|
||||||
|
print("Opening upstream connection to %s" % url)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
try:
|
||||||
|
await client_connect(name, resp)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
counter_timeout_errors.inc()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
counter_cancelled_errors.inc()
|
||||||
|
except asyncio.IncompleteReadError:
|
||||||
|
counter_incomplete_read_errors.inc()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/tiled")
|
||||||
|
async def stream_wrapper(request):
|
||||||
|
async def stream_tiled(response):
|
||||||
|
while True:
|
||||||
|
frames = [value for key, value in sorted(app.ctx.frames.items())]
|
||||||
|
img = tilera(frames, np.vstack([BLANK, np.zeros((30, 1280 // MERGED_SUBSAMPLE, 3))]))
|
||||||
|
_, jpeg = cv2.imencode(".jpg", img, (cv2.IMWRITE_JPEG_QUALITY, 80))
|
||||||
|
data = STREAM_RESPONSE + jpeg.tobytes()
|
||||||
|
await response.write(data)
|
||||||
|
counter_tx_bytes.inc(len(data))
|
||||||
|
counter_tx_frames.inc()
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
return response.stream(
|
||||||
|
stream_tiled,
|
||||||
|
content_type='multipart/x-mixed-replace; boundary=frame'
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.listener('before_server_start')
|
||||||
|
async def setup_db(app, loop):
|
||||||
|
app.ctx.last_frame = None
|
||||||
|
app.ctx.event_frame = asyncio.Event()
|
||||||
|
app.ctx.frames = {}
|
||||||
|
app.ctx.avg = None
|
||||||
|
app.ctx.motion_frames = 0
|
||||||
|
app.ctx.motion_start = None
|
||||||
|
app.ctx.motion_end = None
|
||||||
|
for name, url in targets:
|
||||||
|
app.ctx.frames[name] = None
|
||||||
|
task = asyncio.create_task(client(name, url))
|
||||||
|
|
||||||
|
monitor(app).expose_endpoint()
|
||||||
|
|
||||||
|
try:
|
||||||
|
app.run(port=5001)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
asyncio.get_event_loop().close()
|
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
# All keys here are for dev instance only, do not put prod keys here
|
||||||
|
# To override and use inventory from prod use .env file
|
||||||
|
|
||||||
|
services:
|
||||||
|
camtiler:
|
||||||
|
restart: always
|
||||||
|
network_mode: host
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
entrypoint: /app/camtiler.py
|
||||||
|
command: http://127.0.0.1:8080?action=stream http://127.0.0.2:8080?action=stream http://127.0.0.3:8080?action=stream http://127.0.0.4:8080?action=stream
|
||||||
|
|
||||||
|
mjpg-streamer:
|
||||||
|
network_mode: host
|
||||||
|
restart: always
|
||||||
|
image: kvaps/mjpg-streamer
|
||||||
|
devices:
|
||||||
|
- /dev/video0
|
||||||
|
command: -i "/usr/lib64/input_uvc.so -y -d /dev/video0 -r 1280x720 -f 30" -o "output_http.so"
|
Loading…
Reference in New Issue
Block a user