Spaces:
Running
Running
python codebase + dockerfile + makefile
Browse files- .dockerignore +6 -0
- .gitignore +4 -0
- Dockerfile +20 -0
- Makefile +22 -0
- requirements.txt +8 -0
- src/app.py +71 -0
- src/utils.py +72 -0
- videos/classroom.mp4 +3 -0
.dockerignore
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Docker related
|
2 |
+
Dockerfile
|
3 |
+
docker-compose.yml
|
4 |
+
.dockerignore
|
5 |
+
|
6 |
+
./postgres_data/*
|
.gitignore
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.output
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.12-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
RUN apt-get update && apt-get install -y \
|
6 |
+
libglib2.0-0 \
|
7 |
+
libsm6 \
|
8 |
+
libxext6 \
|
9 |
+
libxrender-dev \
|
10 |
+
libgl1-mesa-glx
|
11 |
+
|
12 |
+
COPY requirements.txt .
|
13 |
+
|
14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
15 |
+
|
16 |
+
COPY . .
|
17 |
+
|
18 |
+
EXPOSE 5000-5100
|
19 |
+
|
20 |
+
CMD ["python", "src/app.py"]
|
Makefile
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
IMAGE_NAME := mock-video-feed
|
2 |
+
CONTAINER_NAME := mock-video-feed-container
|
3 |
+
DOCKERFILE := Dockerfile
|
4 |
+
|
5 |
+
build:
|
6 |
+
docker build -t $(IMAGE_NAME) -f $(DOCKERFILE) .
|
7 |
+
|
8 |
+
run:
|
9 |
+
docker stop $(CONTAINER_NAME) || true
|
10 |
+
docker rm $(CONTAINER_NAME) || true
|
11 |
+
docker run --name $(CONTAINER_NAME) -d -p 5000-5100:5000-5100 $(IMAGE_NAME)
|
12 |
+
|
13 |
+
stop:
|
14 |
+
docker stop $(CONTAINER_NAME) || true
|
15 |
+
docker rm $(CONTAINER_NAME) || true
|
16 |
+
|
17 |
+
clean:
|
18 |
+
docker rmi $(IMAGE_NAME) || true
|
19 |
+
|
20 |
+
rebuild: stop clean build
|
21 |
+
|
22 |
+
.PHONY: build run stop clean rebuild
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
opencv-python==4.11.0.86
|
2 |
+
ultralytics==8.3.133
|
3 |
+
Flask==3.1.0
|
4 |
+
flask-cors==5.0.1
|
5 |
+
PyThreadKiller==3.0.6
|
6 |
+
vidgear==0.3.3
|
7 |
+
selenium==4.32.0
|
8 |
+
webdriver_manager==4.0.2
|
src/app.py
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from vidgear.gears import CamGear
|
2 |
+
from flask import Flask
|
3 |
+
import threading
|
4 |
+
from utils import start_html_stream
|
5 |
+
import os
|
6 |
+
import time
|
7 |
+
import cv2
|
8 |
+
import socket
|
9 |
+
|
10 |
+
def get_video_file():
|
11 |
+
return os.path.join("videos", "classroom.mp4")
|
12 |
+
|
13 |
+
video_file = get_video_file()
|
14 |
+
|
15 |
+
cap = cv2.VideoCapture(video_file)
|
16 |
+
framerate = cap.get(cv2.CAP_PROP_FPS)
|
17 |
+
cap.release()
|
18 |
+
|
19 |
+
stream = CamGear(source=video_file).start()
|
20 |
+
app = Flask(__name__)
|
21 |
+
output_frame = [None]
|
22 |
+
lock = threading.Lock()
|
23 |
+
|
24 |
+
def get_available_port(start_port):
|
25 |
+
port = start_port
|
26 |
+
while True:
|
27 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
28 |
+
if s.connect_ex(('localhost', port)) != 0:
|
29 |
+
return port
|
30 |
+
port += 1
|
31 |
+
|
32 |
+
PORT = get_available_port(5000)
|
33 |
+
|
34 |
+
@app.route('/')
|
35 |
+
def html_stream():
|
36 |
+
return start_html_stream(output_frame, lock)
|
37 |
+
|
38 |
+
def start_flask():
|
39 |
+
app.run(host="0.0.0.0", port=PORT, debug=True, use_reloader=False)
|
40 |
+
|
41 |
+
def process_stream():
|
42 |
+
global stream
|
43 |
+
while True:
|
44 |
+
start_time = time.time()
|
45 |
+
frame = stream.read()
|
46 |
+
if frame is None:
|
47 |
+
stream.stop()
|
48 |
+
stream = CamGear(source=video_file).start()
|
49 |
+
continue
|
50 |
+
|
51 |
+
with lock:
|
52 |
+
output_frame[0] = frame.copy()
|
53 |
+
|
54 |
+
elapsed_time = time.time() - start_time
|
55 |
+
sleep_time = max(1.0 / framerate - elapsed_time, 0)
|
56 |
+
time.sleep(sleep_time)
|
57 |
+
|
58 |
+
def main():
|
59 |
+
flask_thread = threading.Thread(target=start_flask)
|
60 |
+
flask_thread.daemon = True
|
61 |
+
flask_thread.start()
|
62 |
+
|
63 |
+
stream_thread = threading.Thread(target=process_stream)
|
64 |
+
stream_thread.daemon = True
|
65 |
+
stream_thread.start()
|
66 |
+
|
67 |
+
flask_thread.join()
|
68 |
+
stream_thread.join()
|
69 |
+
|
70 |
+
if __name__ == "__main__":
|
71 |
+
main()
|
src/utils.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
from selenium import webdriver
|
4 |
+
from selenium.webdriver.chrome.service import Service
|
5 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
6 |
+
from selenium.webdriver.chrome.options import Options
|
7 |
+
import time
|
8 |
+
from flask import Response
|
9 |
+
import cv2
|
10 |
+
import time
|
11 |
+
|
12 |
+
def start_html_stream(output_frame, lock):
|
13 |
+
def generate():
|
14 |
+
while True:
|
15 |
+
with lock:
|
16 |
+
if output_frame[0] is None:
|
17 |
+
# TODO: Investigate why if remove this print or time.sleep (but not both at the same time), the stream does not work
|
18 |
+
# time.sleep(0.5)
|
19 |
+
print(f"{time.time()} - Frame is None.")
|
20 |
+
continue
|
21 |
+
(flag, encoded_image) = cv2.imencode(".jpg", output_frame[0])
|
22 |
+
if not flag:
|
23 |
+
continue
|
24 |
+
yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encoded_image) + b'\r\n')
|
25 |
+
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
|
26 |
+
|
27 |
+
def capture_webpage(driver):
|
28 |
+
screenshot = driver.get_screenshot_as_png()
|
29 |
+
image = np.frombuffer(screenshot, dtype=np.uint8)
|
30 |
+
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
31 |
+
|
32 |
+
height, width, _ = image.shape
|
33 |
+
new_width = 1920
|
34 |
+
new_height = int(new_width * height / width)
|
35 |
+
|
36 |
+
image = cv2.resize(image, (new_width, new_height))
|
37 |
+
|
38 |
+
if new_height > 1080:
|
39 |
+
start_y = (new_height - 1080) // 2
|
40 |
+
image = image[start_y:start_y + 1080, :]
|
41 |
+
|
42 |
+
return image
|
43 |
+
|
44 |
+
def get_webpage_frames(stream_url):
|
45 |
+
chrome_options = Options()
|
46 |
+
chrome_options.add_argument("--headless")
|
47 |
+
chrome_options.add_argument("--window-size=1920x1080")
|
48 |
+
|
49 |
+
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
|
50 |
+
driver.get(stream_url)
|
51 |
+
|
52 |
+
frames_per_second = 10
|
53 |
+
frame_interval = 1 / frames_per_second
|
54 |
+
|
55 |
+
try:
|
56 |
+
while True:
|
57 |
+
image = capture_webpage(driver)
|
58 |
+
yield image
|
59 |
+
time.sleep(frame_interval)
|
60 |
+
finally:
|
61 |
+
driver.quit()
|
62 |
+
|
63 |
+
def get_mjpeg_frames(stream_url):
|
64 |
+
cap = cv2.VideoCapture(stream_url)
|
65 |
+
|
66 |
+
while True:
|
67 |
+
ret, frame = cap.read()
|
68 |
+
if not ret:
|
69 |
+
break
|
70 |
+
yield frame
|
71 |
+
|
72 |
+
cap.release()
|
videos/classroom.mp4
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a69bd5e39ff0e74286d13bcbfdadddd307b85decd2d9853db7518e0028785e31
|
3 |
+
size 13548133
|