diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index c8fce61..d252c31 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -101,6 +101,7 @@ export class OTelGroupCallMembership { span: Span | undefined; stats: OTelStatsReportEvent[]; }; + private readonly speakingSpans = new Map>(); constructor(private groupCall: GroupCall, client: MatrixClient) { const clientId = client.getUserId(); @@ -125,6 +126,10 @@ export class OTelGroupCallMembership { public onJoinCall() { if (!ElementCallOpenTelemetry.instance) return; + if (this.callMembershipSpan !== undefined) { + logger.warn("Call membership span is already started"); + return; + } // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = @@ -151,10 +156,16 @@ export class OTelGroupCallMembership { } public onLeaveCall() { - this.callMembershipSpan?.addEvent("matrix.leaveCall"); + if (this.callMembershipSpan === undefined) { + logger.warn("Call membership span is already ended"); + return; + } - // and end the main span to indicate we've left - if (this.callMembershipSpan) this.callMembershipSpan.end(); + this.callMembershipSpan.addEvent("matrix.leaveCall"); + // and end the span to indicate we've left + this.callMembershipSpan.end(); + this.callMembershipSpan = undefined; + this.groupCallContext = undefined; } public onUpdateRoomState(event: MatrixEvent) { @@ -302,6 +313,36 @@ export class OTelGroupCallMembership { }); } + public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) { + if (speaking) { + // Ensure that there's an audio activity span for this speaker + let deviceMap = this.speakingSpans.get(member); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.speakingSpans.set(member, deviceMap); + } + + if (!deviceMap.has(deviceId)) { + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.audioActivity", + undefined, + this.groupCallContext + ); + span.setAttribute("matrix.userId", member.userId); + span.setAttribute("matrix.displayName", member.rawDisplayName); + + deviceMap.set(deviceId, span); + } + } else { + // End the audio activity span for this speaker, if any + const deviceMap = this.speakingSpans.get(member); + deviceMap?.get(deviceId)?.end(); + deviceMap?.delete(deviceId); + + if (deviceMap?.size === 0) this.speakingSpans.delete(member); + } + } + public onCallError(error: CallError, call: MatrixCall) { const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 2e0e932..cf1e4dc 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -360,11 +360,11 @@ export function InCallView({ const audioElements: JSX.Element[] = []; if (!spatialAudio || maximisedParticipant) { for (const item of items) { - if (item.isLocal) continue; // We don't want to render own audio audioElements.push( ); diff --git a/src/video-grid/AudioSink.tsx b/src/video-grid/AudioSink.tsx index 24019e4..ef140df 100644 --- a/src/video-grid/AudioSink.tsx +++ b/src/video-grid/AudioSink.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { TileDescriptor } from "./TileDescriptor"; import { useCallFeed } from "./useCallFeed"; import { useMediaStream } from "./useMediaStream"; @@ -23,6 +24,7 @@ import { useMediaStream } from "./useMediaStream"; interface Props { tileDescriptor: TileDescriptor; audioOutput: string; + otelGroupCallMembership?: OTelGroupCallMembership; } // Renders and