This commit is contained in:
		
							
								
								
									
										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 | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| .env | ||||
							
								
								
									
										22
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| 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 \ | ||||
|     libjpeg-dev \ | ||||
|     python3-gevent \ | ||||
|     python3-numpy \ | ||||
|     python3-opencv \ | ||||
|     python3-flask \ | ||||
|     python3-pip \ | ||||
|  && apt-get clean | ||||
| RUN pip3 install boto3 prometheus_client pymongo==3.12.2 aiohttp jpeg2dct sanic==21.6.2 sanic_prometheus motor | ||||
| COPY camdetect.py /app | ||||
| ENTRYPOINT /app/camdetect.py | ||||
| EXPOSE 8000 | ||||
| ENV PYTHONUNBUFFERED=1 | ||||
							
								
								
									
										241
									
								
								camdetect.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										241
									
								
								camdetect.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import aiohttp | ||||
| import asyncio | ||||
| import cv2 | ||||
| import io | ||||
| import numpy as np | ||||
| import os | ||||
| import json | ||||
| import socket | ||||
| import sys | ||||
| from datetime import datetime | ||||
| from jpeg2dct.numpy import load, loads | ||||
| from prometheus_client import Counter, Gauge | ||||
| from sanic import Sanic, response | ||||
| from sanic.response import stream | ||||
| from sanic_prometheus import monitor | ||||
|  | ||||
| _, url = sys.argv | ||||
|  | ||||
| MONGO_URI = os.getenv("MONGO_URI", "mongodb://127.0.0.1:27017/default") | ||||
| FQDN = socket.getfqdn() | ||||
| SLIDE_WINDOW = 2 | ||||
| DCT_BLOCK_SIZE = 8 | ||||
|  | ||||
| # How many blocks have changes to consider movement in frame | ||||
| THRESHOLD_BLOCKS = 20 | ||||
| THRESHOLD_MOTION_START = 2 | ||||
|  | ||||
| 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") | ||||
| counter_movement_frames = Counter( | ||||
|     "camtiler_client_movement_frames", | ||||
|     "Frames with movement detected in them") | ||||
|  | ||||
| gauge_total_blocks = Gauge( | ||||
|     "camtiler_client_total_blocks", | ||||
|     "Total DCT blocks") | ||||
| gauge_active_blocks = Gauge( | ||||
|     "camtiler_client_active_blocks", | ||||
|     "Total active, threshold exceeding DCT blocks") | ||||
|  | ||||
| class Frame(object): | ||||
|     def __init__(self, blob): | ||||
|         self.blob = blob | ||||
|         self.y, self.cb, self.cr = loads(blob) | ||||
|         self.mask = np.int16(self.y[:,:,0]) | ||||
|  | ||||
| async def client_connect(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: | ||||
|                 app.ctx.last_frame = Frame(buf + data[:marker+2]) | ||||
|  | ||||
|                 reference = app.ctx.last_frame.mask | ||||
|                 app.ctx.frames.append(reference) | ||||
|                 if app.ctx.avg is None: | ||||
|                     app.ctx.avg = np.copy(reference) | ||||
|                 else: | ||||
|                     app.ctx.avg += reference | ||||
|  | ||||
|                 if len(app.ctx.frames) > 2 ** SLIDE_WINDOW: | ||||
|                     app.ctx.avg -= app.ctx.frames[0] | ||||
|                     app.ctx.frames = app.ctx.frames[1:] | ||||
|  | ||||
|                 if len(app.ctx.frames) == 2 ** SLIDE_WINDOW: | ||||
|                     app.ctx.thresh = cv2.inRange(cv2.absdiff(app.ctx.last_frame.mask, app.ctx.avg >> SLIDE_WINDOW), 25, 65535) | ||||
|                 else: | ||||
|                     app.ctx.thresh = None | ||||
|                 gauge_total_blocks.set(app.ctx.last_frame.mask.shape[0] * app.ctx.last_frame.mask.shape[1]) | ||||
|  | ||||
|                 movement_detected = False | ||||
|                 if app.ctx.thresh is not None: | ||||
|                     differing_blocks = np.count_nonzero(app.ctx.thresh) | ||||
|                     gauge_active_blocks.set(differing_blocks) | ||||
|                     if differing_blocks > THRESHOLD_BLOCKS: | ||||
|                         counter_movement_frames.inc() | ||||
|                         movement_detected = True | ||||
|  | ||||
|                 if movement_detected: | ||||
|                     if app.ctx.motion_frames < 30: | ||||
|                         app.ctx.motion_frames += 1 | ||||
|                 else: | ||||
|                     if app.ctx.motion_frames > 0: | ||||
|                         app.ctx.motion_frames -= 1 | ||||
|  | ||||
|                 if app.ctx.motion_frames > 20: | ||||
|                     if not app.ctx.motion_start: | ||||
|                         app.ctx.motion_start = datetime.utcnow() | ||||
|                         print("Movement start") | ||||
|                 elif app.ctx.motion_frames < 5: | ||||
|                     app.ctx.motion_start = None | ||||
|                     print("Movement end") | ||||
|  | ||||
|                 app.ctx.event_frame.set() | ||||
|                 app.ctx.event_frame.clear() | ||||
|  | ||||
|                 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(): | ||||
|     print("Opening upstream connection...") | ||||
|     async with aiohttp.ClientSession() as session: | ||||
|         async with session.get(url) as resp: | ||||
|             try: | ||||
|                 await client_connect(resp) | ||||
|             except asyncio.TimeoutError: | ||||
|                 counter_timeout_errors.inc() | ||||
|             except asyncio.CancelledError: | ||||
|                 counter_cancelled_errors.inc() | ||||
|             except asyncio.IncompleteReadError: | ||||
|                 counter_incomplete_read_errors.inc() | ||||
|  | ||||
|  | ||||
| app = Sanic("lease") | ||||
| app.config["WTF_CSRF_ENABLED"] = False | ||||
|  | ||||
| STREAM_RESPONSE = \ | ||||
| b""" | ||||
| --frame | ||||
| Content-Type: image/jpeg | ||||
|  | ||||
| """ | ||||
|  | ||||
| @app.route("/bypass") | ||||
| async def bypass_stream_wrapper(request): | ||||
|     async def stream_camera(response): | ||||
|         while True: | ||||
|             await app.ctx.event_frame.wait() | ||||
|             data = STREAM_RESPONSE + app.ctx.last_frame.blob | ||||
|             await response.write(data) | ||||
|             counter_tx_bytes.inc(len(data)) | ||||
|             counter_tx_frames.inc() | ||||
|  | ||||
|     return response.stream( | ||||
|                 stream_camera, | ||||
|                 content_type='multipart/x-mixed-replace; boundary=frame' | ||||
|             ) | ||||
|  | ||||
| @app.route("/debug") | ||||
| async def stream_wrapper(request): | ||||
|     async def stream_camera(response): | ||||
|         while True: | ||||
|             await app.ctx.event_frame.wait() | ||||
|             img = cv2.imdecode(np.frombuffer(app.ctx.last_frame.blob, dtype=np.uint8), cv2.IMREAD_UNCHANGED) | ||||
|             if len(app.ctx.frames) == 2 ** SLIDE_WINDOW: | ||||
|                 for y in range(0, len(app.ctx.last_frame.mask)): | ||||
|                     for x in range(0, len(app.ctx.last_frame.mask[0])): | ||||
|                         if app.ctx.thresh[y][x] > 0: | ||||
|                             img[y*DCT_BLOCK_SIZE:(y+1)*DCT_BLOCK_SIZE,x*DCT_BLOCK_SIZE:(x+1)*DCT_BLOCK_SIZE,2] = 255 | ||||
|  | ||||
|             _, 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() | ||||
|  | ||||
|     return response.stream( | ||||
|         stream_camera, | ||||
|         content_type='multipart/x-mixed-replace; boundary=frame' | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @app.route('/event') | ||||
| async def wrapper_stream_event(request): | ||||
|     async def stream_event(response): | ||||
|         while True: | ||||
|             await app.ctx.event_frame.wait() | ||||
|             if len(app.ctx.frames) < 2 ** SLIDE_WINDOW: | ||||
|                 continue | ||||
|             s = 'data: ' + json.dumps(app.ctx.thresh.tolist()) + '\r\n\r\n' | ||||
|             await response.write(s.encode()) | ||||
|             counter_tx_events.inc() | ||||
|     return stream(stream_event, content_type='text/event-stream') | ||||
|  | ||||
|  | ||||
| @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 | ||||
|     task = asyncio.create_task(client()) | ||||
|  | ||||
| monitor(app).expose_endpoint() | ||||
|  | ||||
| try: | ||||
|     app.run(port=5000) | ||||
| except KeyboardInterrupt: | ||||
|     asyncio.get_event_loop().close() | ||||
							
								
								
									
										58
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| 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: | ||||
|   camdetect: | ||||
|     restart: always | ||||
|     network_mode: host | ||||
|     build: | ||||
|       context: . | ||||
|     entrypoint: /app/camdetect.py | ||||
|     command: http://user:123456@127.0.0.1:8080?action=stream | ||||
|     environment: | ||||
|       - MJPEGSTREAMER_CREDENTIALS=user:123456 | ||||
|     env_file: .env | ||||
|  | ||||
|   mongoexpress: | ||||
|     restart: always | ||||
|     image: mongo-express | ||||
|     network_mode: host | ||||
|     environment: | ||||
|       - ME_CONFIG_MONGODB_ENABLE_ADMIN=true | ||||
|       - ME_CONFIG_MONGODB_SERVER=127.0.0.1 | ||||
|       - ME_CONFIG_MONGODB_AUTH_DATABASE=admin | ||||
|  | ||||
|   mongo: | ||||
|     network_mode: host | ||||
|     image: mongo:latest | ||||
|     volumes: | ||||
|       - ./mongo-init.sh:/docker-entrypoint-initdb.d/mongo-init.sh:ro | ||||
|     command: mongod --replSet rs0 --bind_ip 127.0.0.1 | ||||
|  | ||||
|   prometheus: | ||||
|     network_mode: host | ||||
|     image: prom/prometheus:latest | ||||
|     command: | ||||
|       - --config.file=/config/prometheus.yml | ||||
|     volumes: | ||||
|       - ./config:/config:ro | ||||
|  | ||||
|   minio: | ||||
|     restart: always | ||||
|     network_mode: host | ||||
|     image: bitnami/minio:latest | ||||
|     environment: | ||||
|       - MINIO_ACCESS_KEY=kspace-mugshot | ||||
|       - MINIO_SECRET_KEY=2mSI6HdbJ8 | ||||
|       - MINIO_DEFAULT_BUCKETS=kspace-mugshot:download | ||||
|       - MINIO_CONSOLE_PORT_NUMBER=9001 | ||||
|  | ||||
|   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 -c user:123456" | ||||
		Reference in New Issue
	
	Block a user