Skip to content

Commit 7b199cc

Browse files
committed
add codec info print.
1 parent a512ae5 commit 7b199cc

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

examples/wxpy_room/wxpy_room.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,43 @@
2424
AudioBandsEvent, EVT_AUDIO_BANDS = wx.lib.newevent.NewEvent()
2525
# Custom event for incoming chat messages
2626
ChatMessageEvent, EVT_CHAT_MESSAGE = wx.lib.newevent.NewEvent()
27+
# Custom event for video codec/stats updates
28+
VideoCodecEvent, EVT_VIDEO_CODEC = wx.lib.newevent.NewEvent()
2729

2830
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wxpy_room_cfg.ini")
2931

32+
def _extract_video_codec(stats):
33+
"""Return (mime_type, implementation) for the active video codec, or (None, None).
34+
35+
`implementation` is `encoder_implementation` for outbound (local) tracks and
36+
`decoder_implementation` for inbound (remote) tracks — e.g. "libvpx", "ExternalDecoder (VideoToolbox)".
37+
"""
38+
codec_id_to_mime = {}
39+
video_codec_id = None
40+
implementation = None
41+
for s in stats:
42+
which = s.WhichOneof("stats")
43+
if which == "codec":
44+
codec_id_to_mime[s.codec.rtc.id] = s.codec.codec.mime_type
45+
elif which == "inbound_rtp" and s.inbound_rtp.stream.kind == "video":
46+
video_codec_id = s.inbound_rtp.stream.codec_id
47+
if s.inbound_rtp.inbound.decoder_implementation:
48+
implementation = s.inbound_rtp.inbound.decoder_implementation
49+
elif which == "outbound_rtp" and s.outbound_rtp.stream.kind == "video":
50+
video_codec_id = s.outbound_rtp.stream.codec_id
51+
if s.outbound_rtp.outbound.encoder_implementation:
52+
implementation = s.outbound_rtp.outbound.encoder_implementation
53+
mime = None
54+
if video_codec_id and video_codec_id in codec_id_to_mime:
55+
mime = codec_id_to_mime[video_codec_id]
56+
else:
57+
for m in codec_id_to_mime.values():
58+
if m.lower().startswith("video/"):
59+
mime = m
60+
break
61+
return mime, implementation
62+
63+
3064
CODEC_MAP = {
3165
"VP8": rtc.VideoCodec.VP8,
3266
"VP9": rtc.VideoCodec.VP9,
@@ -97,6 +131,7 @@ def __init__(self, wx_target):
97131
self._video_streams = {} # participant_identity -> VideoStream
98132
self._audio_streams = {} # participant_identity -> AudioStream
99133
self._track_to_participant = {} # track_sid -> participant_identity
134+
self._stats_tasks = {} # participant_identity -> asyncio.Task (video codec poller)
100135
self._disconnecting = False # True when _async_disconnect is in progress
101136
self._test_video_task = None # asyncio.Task for test video publishing
102137
self._test_video_track = None # LocalVideoTrack
@@ -325,6 +360,9 @@ async def _async_disconnect(self):
325360
for stream in list(self._audio_streams.values()):
326361
await stream.aclose()
327362
self._audio_streams.clear()
363+
for task in list(self._stats_tasks.values()):
364+
task.cancel()
365+
self._stats_tasks.clear()
328366
self._track_to_participant.clear()
329367
await self.room.disconnect()
330368
except Exception:
@@ -348,6 +386,7 @@ def _on_track_subscribed(self, track, publication, participant):
348386
self._track_to_participant[track.sid] = identity
349387
if self.loop:
350388
asyncio.ensure_future(self._receive_video(identity, stream))
389+
self._start_codec_poll(identity, track)
351390
elif track.kind == rtc.TrackKind.KIND_AUDIO:
352391
old_stream = self._audio_streams.pop(identity, None)
353392
if old_stream:
@@ -366,6 +405,7 @@ def _on_track_unsubscribed(self, track, publication, participant):
366405
stream = self._video_streams.pop(identity, None)
367406
if stream:
368407
asyncio.ensure_future(stream.aclose())
408+
self._stop_codec_poll(identity)
369409
elif track.kind == rtc.TrackKind.KIND_AUDIO:
370410
stream = self._audio_streams.pop(identity, None)
371411
if stream:
@@ -384,6 +424,7 @@ def _on_local_track_published(self, publication, track):
384424
self._post_participants()
385425
if self.loop:
386426
asyncio.ensure_future(self._receive_video(identity, stream))
427+
self._start_codec_poll(identity, track)
387428

388429
def _on_local_track_unpublished(self, publication):
389430
logger.info("Local track unpublished: %s", publication.sid)
@@ -393,6 +434,7 @@ def _on_local_track_unpublished(self, publication):
393434
stream = self._video_streams.pop(identity, None)
394435
if stream:
395436
asyncio.ensure_future(stream.aclose())
437+
self._stop_codec_poll(identity)
396438
self._post_participants()
397439

398440
def _on_participant_connected(self, participant):
@@ -408,6 +450,7 @@ def _on_participant_disconnected(self, participant):
408450
stream = self._audio_streams.pop(identity, None)
409451
if stream:
410452
asyncio.ensure_future(stream.aclose())
453+
self._stop_codec_poll(identity)
411454
self._track_to_participant = {
412455
k: v for k, v in self._track_to_participant.items() if v != identity
413456
}
@@ -425,6 +468,9 @@ def _on_disconnected(self, reason):
425468
for stream in list(self._audio_streams.values()):
426469
asyncio.ensure_future(stream.aclose())
427470
self._audio_streams.clear()
471+
for task in list(self._stats_tasks.values()):
472+
task.cancel()
473+
self._stats_tasks.clear()
428474
self._track_to_participant.clear()
429475
self._post_state("disconnected", str(reason))
430476
if self.loop and self.loop.is_running():
@@ -506,6 +552,37 @@ async def _receive_audio(self, identity, stream):
506552
finally:
507553
logger.info("Stopped receiving audio for %s (total frames: %d)", identity, frame_count)
508554

555+
def _start_codec_poll(self, identity, track):
556+
self._stop_codec_poll(identity)
557+
self._stats_tasks[identity] = asyncio.ensure_future(
558+
self._poll_video_codec(identity, track)
559+
)
560+
561+
def _stop_codec_poll(self, identity):
562+
task = self._stats_tasks.pop(identity, None)
563+
if task and not task.done():
564+
task.cancel()
565+
566+
async def _poll_video_codec(self, identity, track):
567+
last = (None, None)
568+
try:
569+
# Brief delay so the first stats sample has data
570+
await asyncio.sleep(1.0)
571+
while True:
572+
try:
573+
stats = await track.get_stats()
574+
codec, impl = _extract_video_codec(stats)
575+
except Exception as e:
576+
logger.debug("get_stats failed for %s: %s", identity, e)
577+
codec, impl = None, None
578+
if codec and (codec, impl) != last:
579+
last = (codec, impl)
580+
evt = VideoCodecEvent(identity=identity, codec=codec, implementation=impl or "")
581+
wx.PostEvent(self.wx_target, evt)
582+
await asyncio.sleep(2.0)
583+
except asyncio.CancelledError:
584+
pass
585+
509586
def _post_state(self, state, message="", **kwargs):
510587
evt = RoomStateEvent(state=state, message=message, **kwargs)
511588
wx.PostEvent(self.wx_target, evt)
@@ -532,6 +609,8 @@ def __init__(self, parent, label="", bg_color=None):
532609
self.label = label
533610
self.video_width = 0
534611
self.video_height = 0
612+
self.video_codec = ""
613+
self.video_codec_impl = ""
535614
self.audio_bands = [0.0] * 5 # 5 frequency band levels (0..1)
536615
self.bitmap = None
537616
self.bg_color = bg_color or wx.Colour(30, 30, 30)
@@ -578,6 +657,11 @@ def on_paint(self, event):
578657
overlay_text = self.label
579658
if self.video_width > 0 and self.video_height > 0:
580659
overlay_text += f" {self.video_width}x{self.video_height}"
660+
if self.video_codec:
661+
if self.video_codec_impl:
662+
overlay_text += f" [{self.video_codec} / {self.video_codec_impl}]"
663+
else:
664+
overlay_text += f" [{self.video_codec}]"
581665
dc.SetTextForeground(wx.WHITE)
582666
dc.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
583667
tw, th = dc.GetTextExtent(overlay_text)
@@ -628,6 +712,16 @@ def update_rgba_frame(self, width, height, rgba_data):
628712
self.bitmap = wx.Bitmap(img)
629713
self.Refresh()
630714

715+
def update_video_codec(self, codec, implementation=""):
716+
"""Set the codec mime_type (e.g. 'video/VP8') and implementation shown in the overlay."""
717+
# Strip the 'video/' prefix for a tighter overlay label.
718+
if codec and codec.lower().startswith("video/"):
719+
codec = codec.split("/", 1)[1]
720+
if codec != self.video_codec or implementation != self.video_codec_impl:
721+
self.video_codec = codec
722+
self.video_codec_impl = implementation
723+
self.Refresh()
724+
631725
def update_audio_bands(self, bands):
632726
"""Update the 5-band audio spectrum levels (list of floats 0..1).
633727
Does not trigger a repaint — the next video frame refresh will draw them.
@@ -678,6 +772,12 @@ def update_audio_bands(self, identity, bands):
678772
if panel:
679773
panel.update_audio_bands(bands)
680774

775+
def update_video_codec(self, identity, codec, implementation=""):
776+
"""Update a participant's video panel codec overlay."""
777+
panel = self.video_panels.get(identity)
778+
if panel:
779+
panel.update_video_codec(codec, implementation)
780+
681781
def update_video(self, identity, bitmap, label=None):
682782
panel = self.video_panels.get(identity)
683783
if panel:
@@ -1007,6 +1107,7 @@ def __init__(self):
10071107
self.Bind(EVT_PARTICIPANT, self.on_participant_update)
10081108
self.Bind(EVT_AUDIO_BANDS, self.on_audio_bands)
10091109
self.Bind(EVT_CHAT_MESSAGE, self.on_chat_message)
1110+
self.Bind(EVT_VIDEO_CODEC, self.on_video_codec)
10101111

10111112
self.Centre()
10121113

@@ -1075,6 +1176,10 @@ def on_audio_bands(self, event):
10751176
"""Handle audio spectrum bands from async thread."""
10761177
self.video_grid.update_audio_bands(event.identity, event.bands)
10771178

1179+
def on_video_codec(self, event):
1180+
"""Handle video codec updates from async thread."""
1181+
self.video_grid.update_video_codec(event.identity, event.codec, event.implementation)
1182+
10781183
def on_chat_message(self, event):
10791184
self.right_panel.append_chat(event.sender, event.message)
10801185

0 commit comments

Comments
 (0)