Web client: video from a call's other side no longer stays black
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s

In an encrypted call, video that arrives before its sender's key is
dropped, keyframe and all, and the decoder waits for another keyframe
that may never come. When a participant's first key arrives, subscribe
to their video again so the server asks for a fresh one.

Also keep your own tile in the corner of a call of two: the tile's own
"relative" class won over the "absolute" it was given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-11-02 10:39:19 +01:00
1 parent 4dd91c4f2c
commit 994e6afd49
2 files changed
+23 -8

No files matched your search

+4 -6
View File
@@ -195,12 +195,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
<!-- Two of you: them on the stage, you in the corner. -->
<div v-else-if="pair && remotes[0]" :class="['relative', expanded ? 'h-full' : 'aspect-video']">
<CallTile :tile="remotes[0]" :compact="!expanded" class="h-full w-full" />
<CallTile
v-if="self"
:tile="self"
compact
class="absolute right-3 bottom-3 aspect-video w-1/4 min-w-28 shadow-float"
/>
<!-- A wrapper: the tile's own "relative" would win over "absolute". -->
<div v-if="self" class="absolute right-3 bottom-3 aspect-video w-1/4 min-w-28 shadow-float">
<CallTile :tile="self" compact class="h-full w-full" />
</div>
</div>
<!-- More: a grid, or whoever is speaking when shrunk. -->
+19 -2
View File
@@ -455,8 +455,24 @@ export const useCall = defineStore('call', () => {
})
}
function onEncryptionKey(key: Uint8Array, index: number, _membership: unknown, backendIdentity: string) {
void keyProvider?.setParticipantKey(key, index, backendIdentity)
/** Remote participants whose media key has arrived. */
const keyed = new Set<string>()
async function onEncryptionKey(key: Uint8Array, index: number, _membership: unknown, backendIdentity: string) {
const provider = keyProvider
if (!provider) return
await provider.setParticipantKey(key, index, backendIdentity)
if (keyProvider !== provider || keyed.has(backendIdentity)) return
keyed.add(backendIdentity)
// Video that came before its key was dropped, keyframe and all, and
// the decoder waits for another that may never come: black, while the
// audio plays on. Subscribing again makes the server ask for one.
const p = lk.value?.remoteParticipants.get(backendIdentity)
for (const pub of p?.videoTrackPublications.values() ?? []) {
if (!pub.isSubscribed) continue
pub.setSubscribed(false)
pub.setSubscribed(true)
}
}
function onMembershipError(e: unknown) {
@@ -573,6 +589,7 @@ export const useCall = defineStore('call', () => {
session = null
lk.value = null
keyProvider = null
keyed.clear()
sentNotification = null
hadCompany = false
window.clearInterval(lengthTimer)