File size: 4,810 Bytes
6a298df
90eeca9
6a298df
3269f7b
227eeeb
ac394ea
e2c8d12
90eeca9
6a298df
64b5200
6a298df
64b5200
d1bc6a5
64b5200
 
d1bc6a5
6a298df
90eeca9
ac394ea
90eeca9
e62e725
6a298df
95b7f7e
 
 
 
 
 
 
 
 
 
 
 
018bd02
95b7f7e
 
 
 
 
018bd02
6a298df
 
e62e725
6a298df
 
018bd02
6a298df
 
 
 
 
 
 
 
 
 
 
 
40c772f
 
6a298df
c3dd12a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95b7f7e
c3dd12a
 
 
78f6c9b
11d27a3
e2c8d12
6a298df
 
 
e62e725
6a298df
 
 
606bd1d
11d27a3
606bd1d
d1bc6a5
 
 
 
 
 
 
 
3269f7b
9f27d12
a8ac2a3
6a298df
84f1606
018bd02
78f6c9b
90eeca9
018bd02
 
 
 
e62e725
64b5200
e62e725
018bd02
d1bc6a5
e62e725
d1bc6a5
6a298df
 
d4a39d3
40c772f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d68c77
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from flask import Flask, Response, stream_with_context, request
import cv2
from cv2.typing import MatLike
import threading
import time
import os
import psutil

class Session:
    def __init__(self, id: str):
        self.id = id
        self.latest_frame: MatLike = None
        self.published_frame: MatLike = None
        self.measured_fps = 0
        self.cpu_usage = 0
        self.show_stats = True

app = Flask(__name__)
PORT = int(os.environ.get("PORT", 5000))

sessions: dict[str, Session] = {}

def draw_stats(frame: MatLike, session: Session):
    font = cv2.FONT_HERSHEY_SIMPLEX
    font_scale = 1
    thickness = 2
    color = (0, 255, 0)
    text = f"""
    ID: {session.id}
    FPS: {session.measured_fps:.0f}
    CPU: {session.cpu_usage:02.0f}%
    """

    y0, dy = 30, 36
    for i, line in enumerate(text.split("\n")):
        (text_width, text_height), _ = cv2.getTextSize(line, font, font_scale, thickness)
        x = frame.shape[1] - text_width - 10
        y = y0 + (i - 1) * dy
        cv2.putText(frame, line, (x, y), font, font_scale, color, thickness, cv2.LINE_AA)

def process_frame(session_id: str, video_path: str):
    global sessions

    session: Session = sessions.get(session_id, None)

    if session is not None:
        cap = cv2.VideoCapture(video_path)
        framerate = cap.get(cv2.CAP_PROP_FPS)
        frame_duration = 1 / framerate

        # variables to measure FPS
        frame_count = 0
        last_time = time.time()
        rolling_duration = 0.5

        # variables to control framerate
        next_frame_time = time.time()
        
        while True:
            if session_id not in sessions:
                break
            # control framerate
            if time.time() >= next_frame_time:
                ret, frame = cap.read()

                # measure FPS
                frame_count += 1
                now = time.time()
                if now - last_time >= rolling_duration:  
                    session.measured_fps = frame_count / (now - last_time)
                    session.cpu_usage = psutil.cpu_percent(interval=None)
                    frame_count = 0
                    last_time = now

                if not ret:
                    cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                    continue
                else:
                    if session.show_stats:
                        draw_stats(frame, session)
                session.latest_frame = frame

                next_frame_time += frame_duration
            else:
                time.sleep(0.01)

def stream_mjpeg(session_id: str):
    global sessions

    session: Session = sessions.get(session_id, None)

    if session is not None:
        while True:
            if session.published_frame is session.latest_frame:
                time.sleep(0.01)
            else:
                if session.latest_frame is None:
                    continue
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50]
                ret, jpeg = cv2.imencode(".jpg", session.latest_frame, encode_param)
                frame_bytes = jpeg.tobytes() if ret else None
                if frame_bytes is not None:
                    session.published_frame = session.latest_frame
                    yield (b"--frame\r\n"b"Content-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n")
            
@app.get("/")
def get_video():
    global sessions

    session_id = request.args.get("video_index", "0")
    show_stats = request.args.get("show_stats", "1") == "1"

    video_index = int(request.args.get("video_index", 0)) % 4
    video_files = sorted([f for f in os.listdir("videos") if f.lower().endswith((".mp4", ".avi", ".mov"))])
    video_path = os.path.join("videos", video_files[int(video_index)])

    if session_id not in sessions:
        session = Session(session_id)
        sessions[session_id] = session
        threading.Thread(target=process_frame, args=[session_id, video_path], daemon=True).start()
    else:
        session = sessions[session_id]
        session.show_stats = show_stats
        
    return Response(stream_with_context(stream_mjpeg(session_id)), mimetype="multipart/x-mixed-replace; boundary=frame")

@app.get("/sessions")
def get_sessions():
    global sessions
    return {
        "sessions": [session.id for session in sessions.values()]
    }

@app.get("/threads")
def get_threads():
    threads = threading.enumerate()
    return {
        "threads_count": len(threads)
    }

@app.get("/killall")
def kill_all():
    global sessions
    sessions.clear()
    for thread in threading.enumerate():
        if thread is not threading.current_thread():
            thread.join(0.1)
    return {"status": "all sessions cleared and threads joined"}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=PORT, threaded=True)