Skip to content

Commit b945133

Browse files
committed
feat: add 'frame_delay' to stats
This commit adds the frame delay between the processing time and the expected presentation time to the `/stats` endpoints.
1 parent 19fc0ac commit b945133

File tree

2 files changed

+66
-26
lines changed

2 files changed

+66
-26
lines changed

server/app.py

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
import time
2323

2424
logger = logging.getLogger(__name__)
25-
logging.getLogger('aiortc.rtcrtpsender').setLevel(logging.WARNING)
26-
logging.getLogger('aiortc.rtcrtpreceiver').setLevel(logging.WARNING)
25+
logging.getLogger("aiortc").setLevel(logging.DEBUG)
26+
logging.getLogger("aiortc.rtcrtpsender").setLevel(logging.WARNING)
27+
logging.getLogger("aiortc.rtcrtpreceiver").setLevel(logging.WARNING)
2728

2829

2930
MAX_BITRATE = 2000000
@@ -51,36 +52,60 @@ def __init__(self, track: MediaStreamTrack, pipeline: Pipeline):
5152
super().__init__()
5253
self.track = track
5354
self.pipeline = pipeline
54-
self._frame_count = 0
55-
self._start_time = time.monotonic()
5655
self._lock = threading.Lock()
5756
self._fps = 0.0
57+
self._frame_delay = 0.0
5858
self._running = True
59+
self._fps_interval_frame_count = 0
60+
self._last_fps_calculation_time = time.monotonic()
61+
self._stream_start_time = None
62+
self._last_frame_presentation_time = None
63+
self._last_frame_processed_time = None
5964
self._start_fps_thread()
65+
self._start_frame_delay_thread()
6066

6167
def _start_fps_thread(self):
6268
"""Start a separate thread to calculate FPS periodically."""
63-
self.fps_thread = threading.Thread(target=self._calculate_fps_loop, daemon=True)
64-
self.fps_thread.start()
69+
self._fps_thread = threading.Thread(
70+
target=self._calculate_fps_loop, daemon=True
71+
)
72+
self._fps_thread.start()
6573

6674
def _calculate_fps_loop(self):
6775
"""Loop to calculate FPS periodically."""
6876
while self._running:
6977
time.sleep(1) # Calculate FPS every second.
7078
with self._lock:
7179
current_time = time.monotonic()
72-
time_diff = current_time - self._start_time
80+
time_diff = current_time - self._last_fps_calculation_time
7381
if time_diff > 0:
74-
self._fps = self._frame_count / time_diff
82+
self._fps = self._fps_interval_frame_count / time_diff
7583

7684
# Reset start_time and frame_count for the next interval.
77-
self._start_time = current_time
78-
self._frame_count = 0
85+
self._last_fps_calculation_time = current_time
86+
self._fps_interval_frame_count = 0
87+
88+
def _start_frame_delay_thread(self):
89+
"""Start a separate thread to calculate frame delay periodically."""
90+
self._frame_delay_thread = threading.Thread(
91+
target=self._calculate_frame_delay_loop, daemon=True
92+
)
93+
self._frame_delay_thread.start()
94+
95+
def _calculate_frame_delay_loop(self):
96+
"""Loop to calculate frame delay periodically."""
97+
while self._running:
98+
time.sleep(1) # Calculate frame delay every second.
99+
with self._lock:
100+
if self._last_frame_presentation_time is not None:
101+
current_time = time.monotonic()
102+
self._frame_delay = (current_time - self._stream_start_time ) - float(self._last_frame_presentation_time)
79103

80104
def stop(self):
81105
"""Stop the FPS calculation thread."""
82106
self._running = False
83-
self.fps_thread.join()
107+
self._fps_thread.join()
108+
self._frame_delay_thread.join()
84109

85110
@property
86111
def fps(self) -> float:
@@ -92,19 +117,34 @@ def fps(self) -> float:
92117
with self._lock:
93118
return self._fps
94119

120+
@property
121+
def frame_delay(self) -> float:
122+
"""Get the current frame delay.
123+
124+
Returns:
125+
The current frame delay.
126+
"""
127+
with self._lock:
128+
return self._frame_delay
129+
95130
async def recv(self) -> av.VideoFrame:
96131
"""Receive and process a video frame. Called by the WebRTC library when a frame
97132
is received.
98133
99134
Returns:
100135
The processed video frame.
101136
"""
137+
if self._stream_start_time is None:
138+
self._stream_start_time = time.monotonic()
139+
102140
input_frame = await self.track.recv()
103141
processed_frame = await self.pipeline(input_frame)
104142

105-
# Increment frame count for FPS calculation.
143+
# Store frame info for stats.
106144
with self._lock:
107-
self._frame_count += 1
145+
self._fps_interval_frame_count += 1
146+
self._last_frame_presentation_time = input_frame.time
147+
self._last_frame_processed_time = time.monotonic()
108148

109149
return processed_frame
110150

@@ -187,30 +227,29 @@ async def offer(request):
187227
@pc.on("datachannel")
188228
def on_datachannel(channel):
189229
if channel.label == "control":
230+
190231
@channel.on("message")
191232
async def on_message(message):
192233
try:
193234
params = json.loads(message)
194-
235+
195236
if params.get("type") == "get_nodes":
196237
nodes_info = await pipeline.get_nodes_info()
197-
response = {
198-
"type": "nodes_info",
199-
"nodes": nodes_info
200-
}
238+
response = {"type": "nodes_info", "nodes": nodes_info}
201239
channel.send(json.dumps(response))
202240
elif params.get("type") == "update_prompt":
203241
if "prompt" not in params:
204-
logger.warning("[Control] Missing prompt in update_prompt message")
242+
logger.warning(
243+
"[Control] Missing prompt in update_prompt message"
244+
)
205245
return
206246
pipeline.set_prompt(params["prompt"])
207-
response = {
208-
"type": "prompt_updated",
209-
"success": True
210-
}
247+
response = {"type": "prompt_updated", "success": True}
211248
channel.send(json.dumps(response))
212249
else:
213-
logger.warning("[Server] Invalid message format - missing required fields")
250+
logger.warning(
251+
"[Server] Invalid message format - missing required fields"
252+
)
214253
except json.JSONDecodeError:
215254
logger.error("[Server] Invalid JSON received")
216255
except Exception as e:
@@ -309,8 +348,8 @@ async def on_shutdown(app: web.Application):
309348

310349
logging.basicConfig(
311350
level=args.log_level.upper(),
312-
format='%(asctime)s [%(levelname)s] %(message)s',
313-
datefmt='%H:%M:%S'
351+
format="%(asctime)s [%(levelname)s] %(message)s",
352+
datefmt="%H:%M:%S",
314353
)
315354

316355
app = web.Application()

server/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def get_video_track_stats(self, video_track: MediaStreamTrack) -> Dict[str, Any]
8585
"""
8686
return {
8787
"fps": video_track.fps,
88+
"frame_delay": video_track.frame_delay,
8889
}
8990

9091
async def get_stats(self, _) -> web.Response:

0 commit comments

Comments
 (0)