fix(MAT-164): proactive key poll on screen share + faster DEC_FAILED recovery

When a video track is subscribed (screen share starts), Element Call
rotates the E2EE key. Instead of waiting for DEC_FAILED, proactively
poll the timeline for the new key (6x @ 500ms = 3s window).

Also reduce DEC_FAILED threshold from 3→1 and cooldown from 5s→2s
for faster recovery when the proactive poll misses the rotation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-19 07:44:51 +02:00
parent de3d67f756
commit f27d545012

View File

@@ -709,6 +709,27 @@ class VoiceSession:
track_source = getattr(pub, 'source', None) or "unknown"
self._video_track = t
logger.info("Video track stored from %s source=%s for on-demand vision", p.identity, track_source)
# Screen share starts → Element Call rotates E2EE key.
# Proactively poll timeline for the new key instead of waiting
# for DEC_FAILED (MAT-164).
async def _proactive_key_poll(pid=p.identity):
pre_key = self._caller_key
for attempt in range(6): # 6 × 500ms = 3s
await asyncio.sleep(0.5)
if self._caller_key != pre_key:
logger.info("Proactive poll: key rotated via sync (attempt %d)", attempt + 1)
return
new_key = await self._fetch_encryption_key_http()
if new_key and new_key != pre_key:
logger.info("Proactive poll: got new key from timeline (attempt %d, %s)",
attempt + 1, new_key.hex()[:8])
self.on_encryption_key(
self._caller_identity.split(":")[0] if self._caller_identity else "",
self._caller_identity.split(":")[-1] if self._caller_identity else "",
new_key, 0)
return
logger.info("Proactive poll: no key rotation after 3s")
asyncio.ensure_future(_proactive_key_poll())
if int(t.kind) in (1, 2) and e2ee_opts is not None: # audio + video tracks
caller_id = p.identity
track_type = "video" if int(t.kind) == 2 else "audio"
@@ -754,8 +775,8 @@ class VoiceSession:
now = time.monotonic()
if int(state) == 3:
_dec_failed_count[p_id] = _dec_failed_count.get(p_id, 0) + 1
# After 3+ DEC_FAILED: re-fetch key from timeline (key may have rotated)
if _dec_failed_count[p_id] >= 3 and not _refetch_in_progress:
# After 1+ DEC_FAILED: re-fetch key from timeline (key may have rotated)
if _dec_failed_count[p_id] >= 1 and not _refetch_in_progress:
_refetch_in_progress = True
_p_id_copy = p_id # capture for closure
async def _refetch_key():
@@ -781,9 +802,9 @@ class VoiceSession:
finally:
_refetch_in_progress = False
asyncio.ensure_future(_refetch_key())
# Cooldown: only re-key every 5s to avoid tight loops
# Cooldown: only re-key every 2s to avoid tight loops
last = _last_rekey_time.get(p_id, 0)
if (now - last) < 5.0:
if (now - last) < 2.0:
return
_last_rekey_time[p_id] = now
if self._caller_all_keys: