Merge pull request #971 from robintown/audio-observability
End-to-end audio observability
This commit is contained in:
		
				commit
				
					
						858c68baf1
					
				
			
		
					 4 changed files with 81 additions and 26 deletions
				
			
		| 
						 | 
					@ -101,6 +101,7 @@ export class OTelGroupCallMembership {
 | 
				
			||||||
    span: Span | undefined;
 | 
					    span: Span | undefined;
 | 
				
			||||||
    stats: OTelStatsReportEvent[];
 | 
					    stats: OTelStatsReportEvent[];
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private groupCall: GroupCall, client: MatrixClient) {
 | 
					  constructor(private groupCall: GroupCall, client: MatrixClient) {
 | 
				
			||||||
    const clientId = client.getUserId();
 | 
					    const clientId = client.getUserId();
 | 
				
			||||||
| 
						 | 
					@ -125,6 +126,10 @@ export class OTelGroupCallMembership {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public onJoinCall() {
 | 
					  public onJoinCall() {
 | 
				
			||||||
    if (!ElementCallOpenTelemetry.instance) return;
 | 
					    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
 | 
					    // Create the main span that tracks the time we intend to be in the call
 | 
				
			||||||
    this.callMembershipSpan =
 | 
					    this.callMembershipSpan =
 | 
				
			||||||
| 
						 | 
					@ -151,10 +156,16 @@ export class OTelGroupCallMembership {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public onLeaveCall() {
 | 
					  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
 | 
					    this.callMembershipSpan.addEvent("matrix.leaveCall");
 | 
				
			||||||
    if (this.callMembershipSpan) this.callMembershipSpan.end();
 | 
					    // and end the span to indicate we've left
 | 
				
			||||||
 | 
					    this.callMembershipSpan.end();
 | 
				
			||||||
 | 
					    this.callMembershipSpan = undefined;
 | 
				
			||||||
 | 
					    this.groupCallContext = undefined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public onUpdateRoomState(event: MatrixEvent) {
 | 
					  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) {
 | 
					  public onCallError(error: CallError, call: MatrixCall) {
 | 
				
			||||||
    const callTrackingInfo = this.callsByCallId.get(call.callId);
 | 
					    const callTrackingInfo = this.callsByCallId.get(call.callId);
 | 
				
			||||||
    if (!callTrackingInfo) {
 | 
					    if (!callTrackingInfo) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -360,11 +360,11 @@ export function InCallView({
 | 
				
			||||||
  const audioElements: JSX.Element[] = [];
 | 
					  const audioElements: JSX.Element[] = [];
 | 
				
			||||||
  if (!spatialAudio || maximisedParticipant) {
 | 
					  if (!spatialAudio || maximisedParticipant) {
 | 
				
			||||||
    for (const item of items) {
 | 
					    for (const item of items) {
 | 
				
			||||||
      if (item.isLocal) continue; // We don't want to render own audio
 | 
					 | 
				
			||||||
      audioElements.push(
 | 
					      audioElements.push(
 | 
				
			||||||
        <AudioSink
 | 
					        <AudioSink
 | 
				
			||||||
          tileDescriptor={item}
 | 
					          tileDescriptor={item}
 | 
				
			||||||
          audioOutput={audioOutput}
 | 
					          audioOutput={audioOutput}
 | 
				
			||||||
 | 
					          otelGroupCallMembership={otelGroupCallMembership}
 | 
				
			||||||
          key={item.id}
 | 
					          key={item.id}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
 | 
				
			||||||
import { TileDescriptor } from "./TileDescriptor";
 | 
					import { TileDescriptor } from "./TileDescriptor";
 | 
				
			||||||
import { useCallFeed } from "./useCallFeed";
 | 
					import { useCallFeed } from "./useCallFeed";
 | 
				
			||||||
import { useMediaStream } from "./useMediaStream";
 | 
					import { useMediaStream } from "./useMediaStream";
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,7 @@ import { useMediaStream } from "./useMediaStream";
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  tileDescriptor: TileDescriptor;
 | 
					  tileDescriptor: TileDescriptor;
 | 
				
			||||||
  audioOutput: string;
 | 
					  audioOutput: string;
 | 
				
			||||||
 | 
					  otelGroupCallMembership?: OTelGroupCallMembership;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Renders and <audio> element on the page playing the given stream
 | 
					// Renders and <audio> element on the page playing the given stream
 | 
				
			||||||
| 
						 | 
					@ -30,8 +32,12 @@ interface Props {
 | 
				
			||||||
export const AudioSink: React.FC<Props> = ({
 | 
					export const AudioSink: React.FC<Props> = ({
 | 
				
			||||||
  tileDescriptor,
 | 
					  tileDescriptor,
 | 
				
			||||||
  audioOutput,
 | 
					  audioOutput,
 | 
				
			||||||
 | 
					  otelGroupCallMembership,
 | 
				
			||||||
}: Props) => {
 | 
					}: Props) => {
 | 
				
			||||||
  const { localVolume, stream } = useCallFeed(tileDescriptor.callFeed);
 | 
					  const { localVolume, stream } = useCallFeed(
 | 
				
			||||||
 | 
					    tileDescriptor.callFeed,
 | 
				
			||||||
 | 
					    otelGroupCallMembership
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const audioElementRef = useMediaStream(
 | 
					  const audioElementRef = useMediaStream(
 | 
				
			||||||
    stream,
 | 
					    stream,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,8 @@ import { useState, useEffect } from "react";
 | 
				
			||||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
					import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
				
			||||||
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
 | 
					import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CallFeedState {
 | 
					interface CallFeedState {
 | 
				
			||||||
  callFeed: CallFeed | undefined;
 | 
					  callFeed: CallFeed | undefined;
 | 
				
			||||||
  isLocal: boolean;
 | 
					  isLocal: boolean;
 | 
				
			||||||
| 
						 | 
					@ -46,40 +48,46 @@ function getCallFeedState(callFeed: CallFeed | undefined): CallFeedState {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
 | 
					export function useCallFeed(
 | 
				
			||||||
 | 
					  callFeed: CallFeed | undefined,
 | 
				
			||||||
 | 
					  otelGroupCallMembership?: OTelGroupCallMembership
 | 
				
			||||||
 | 
					): CallFeedState {
 | 
				
			||||||
  const [state, setState] = useState<CallFeedState>(() =>
 | 
					  const [state, setState] = useState<CallFeedState>(() =>
 | 
				
			||||||
    getCallFeedState(callFeed)
 | 
					    getCallFeedState(callFeed)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    function onSpeaking(speaking: boolean) {
 | 
					 | 
				
			||||||
      setState((prevState) => ({ ...prevState, speaking }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function onMuteStateChanged(audioMuted: boolean, videoMuted: boolean) {
 | 
					 | 
				
			||||||
      setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function onLocalVolumeChanged(localVolume: number) {
 | 
					 | 
				
			||||||
      setState((prevState) => ({ ...prevState, localVolume }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function onUpdateCallFeed() {
 | 
					    function onUpdateCallFeed() {
 | 
				
			||||||
      setState(getCallFeedState(callFeed));
 | 
					      setState(getCallFeedState(callFeed));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onUpdateCallFeed();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (callFeed) {
 | 
					    if (callFeed) {
 | 
				
			||||||
 | 
					      const onSpeaking = (speaking: boolean) => {
 | 
				
			||||||
 | 
					        otelGroupCallMembership?.onSpeaking(
 | 
				
			||||||
 | 
					          callFeed.getMember()!,
 | 
				
			||||||
 | 
					          callFeed.deviceId!,
 | 
				
			||||||
 | 
					          speaking
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        setState((prevState) => ({ ...prevState, speaking }));
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const onMuteStateChanged = (audioMuted: boolean, videoMuted: boolean) => {
 | 
				
			||||||
 | 
					        setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const onLocalVolumeChanged = (localVolume: number) => {
 | 
				
			||||||
 | 
					        setState((prevState) => ({ ...prevState, localVolume }));
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      callFeed.on(CallFeedEvent.Speaking, onSpeaking);
 | 
					      callFeed.on(CallFeedEvent.Speaking, onSpeaking);
 | 
				
			||||||
      callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
 | 
					      callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
 | 
				
			||||||
      callFeed.on(CallFeedEvent.LocalVolumeChanged, onLocalVolumeChanged);
 | 
					      callFeed.on(CallFeedEvent.LocalVolumeChanged, onLocalVolumeChanged);
 | 
				
			||||||
      callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
 | 
					      callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
 | 
				
			||||||
      callFeed.on(CallFeedEvent.Disposed, onUpdateCallFeed);
 | 
					      callFeed.on(CallFeedEvent.Disposed, onUpdateCallFeed);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onUpdateCallFeed();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return () => {
 | 
					      return () => {
 | 
				
			||||||
      if (callFeed) {
 | 
					 | 
				
			||||||
        callFeed.removeListener(CallFeedEvent.Speaking, onSpeaking);
 | 
					        callFeed.removeListener(CallFeedEvent.Speaking, onSpeaking);
 | 
				
			||||||
        callFeed.removeListener(
 | 
					        callFeed.removeListener(
 | 
				
			||||||
          CallFeedEvent.MuteStateChanged,
 | 
					          CallFeedEvent.MuteStateChanged,
 | 
				
			||||||
| 
						 | 
					@ -90,9 +98,9 @@ export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
 | 
				
			||||||
          onLocalVolumeChanged
 | 
					          onLocalVolumeChanged
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
 | 
					        callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
  }, [callFeed]);
 | 
					    }
 | 
				
			||||||
 | 
					  }, [callFeed, otelGroupCallMembership]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state;
 | 
					  return state;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue