66import os
77import signal
88import time
9+ from enum import Enum
910
1011import numpy as np
1112from livekit import api , rtc
1920 ) from exc
2021
2122
23+ class FrameFormat (str , Enum ):
24+ """Frame format to use for publishing."""
25+
26+ RGBA = "rgba"
27+ I420 = "i420"
28+
29+
2230def parse_args () -> argparse .Namespace :
2331 parser = argparse .ArgumentParser (
2432 description = "Publish a local camera track with optional frame metadata." ,
@@ -46,6 +54,13 @@ def parse_args() -> argparse.Namespace:
4654 action = "store_true" ,
4755 help = "Attach a monotonically increasing FrameMetadata.frame_id" ,
4856 )
57+ parser .add_argument (
58+ "--format" ,
59+ type = FrameFormat ,
60+ default = FrameFormat .I420 ,
61+ choices = list (FrameFormat ),
62+ help = "Frame format: 'i420' (default, efficient) or 'rgba' (original behavior)" ,
63+ )
4964 return parser .parse_args ()
5065
5166
@@ -147,6 +162,14 @@ def _install_signal_handlers(stop_event: asyncio.Event) -> None:
147162 pass
148163
149164
165+ def _bgr_to_i420 (bgr : np .ndarray , width : int , height : int ) -> bytes :
166+ """Convert BGR image to I420 (YUV420p) format efficiently."""
167+ # OpenCV's COLOR_BGR2YUV_I420 outputs planar YUV420p (I420)
168+ # Layout: Y plane (width*height), U plane (width/2 * height/2), V plane (width/2 * height/2)
169+ yuv = cv2 .cvtColor (bgr , cv2 .COLOR_BGR2YUV_I420 )
170+ return yuv .tobytes ()
171+
172+
150173async def _capture_loop (
151174 args : argparse .Namespace ,
152175 capture : cv2 .VideoCapture ,
@@ -161,6 +184,9 @@ async def _capture_loop(
161184 frame_id = 1
162185 submitted = 0
163186 last_log_at = time .perf_counter ()
187+ use_i420 = args .format == FrameFormat .I420
188+
189+ logging .info ("Using frame format: %s" , args .format .value )
164190
165191 while not stop_event .is_set ():
166192 ok , bgr = await asyncio .to_thread (capture .read )
@@ -174,9 +200,17 @@ async def _capture_loop(
174200
175201 user_timestamp = _unix_time_us ()
176202 timestamp_us = (time .perf_counter_ns () - started_at_ns ) // 1_000
177- rgba = cv2 .cvtColor (bgr , cv2 .COLOR_BGR2RGBA )
178- rgba = np .ascontiguousarray (rgba )
179- frame = rtc .VideoFrame (width , height , rtc .VideoBufferType .RGBA , rgba .tobytes ())
203+
204+ if use_i420 :
205+ # Direct BGR -> I420 conversion (single step, more efficient)
206+ i420_data = _bgr_to_i420 (bgr , width , height )
207+ frame = rtc .VideoFrame (width , height , rtc .VideoBufferType .I420 , i420_data )
208+ else :
209+ # Original path: BGR -> RGBA -> I420 (in FFI layer)
210+ rgba = cv2 .cvtColor (bgr , cv2 .COLOR_BGR2RGBA )
211+ rgba = np .ascontiguousarray (rgba )
212+ frame = rtc .VideoFrame (width , height , rtc .VideoBufferType .RGBA , rgba .tobytes ())
213+
180214 metadata = _metadata_for_frame (
181215 args ,
182216 user_timestamp = user_timestamp ,
@@ -191,7 +225,10 @@ async def _capture_loop(
191225 now = time .perf_counter ()
192226 if now - last_log_at >= 2.0 :
193227 logging .info (
194- "published %s frames at ~%.1f fps" , submitted , submitted / (now - last_log_at )
228+ "published %s frames at ~%.1f fps (format=%s)" ,
229+ submitted ,
230+ submitted / (now - last_log_at ),
231+ args .format .value ,
195232 )
196233 submitted = 0
197234 last_log_at = now
0 commit comments