import Logger from '../logger/Logger'; import { MeetingSessionTimingObserver } from './MeetingSessionTiming'; /** * MeetingSessionTimingManager tracks all lifecycle timestamps for a meeting session * and emits them in batches via the observer. * * A batch begins when the first event is recorded (e.g. onStart, onRemoteVideoAdded) * and completes when all tracked categories have reached their terminal state * (e.g. signaling fully connected, audio first packet received, video first frame * rendered). If a batch does not complete within TIMEOUT_THRESHOLD_MS (15 s), * it is emitted with per-category timedOut flags. * * After each emission, already-reported state is cleared so that subsequent events * (e.g. a mid-call remote video add) trigger a new batch containing only new data. * * Categories are only included in a batch if their corresponding on*Added method * was called. For example, if local video is never started, the batch will not * wait for local video timing. Remote video entries that were never bound to a * video element are silently omitted. */ export default class MeetingSessionTimingManager { private static readonly TIMEOUT_THRESHOLD_MS; private observers; private logger; private batchTimeout; private signalingTiming; private isResubscribe; private remoteAudioTiming; private localAudioTiming; private localVideoTiming; private localVideoHasEmitted; private expectingRemoteVideo; private remoteVideoTiming; private boundRemoteVideoGroupIds; private emittedRemoteVideoGroupIds; constructor(logger: Logger); /** * Adds an observer to receive timing data notifications. */ addObserver(observer: MeetingSessionTimingObserver): void; removeObserver(observer: MeetingSessionTimingObserver): void; /** * Starts the batch timer if not already started. * Called by onStart() and any on*Added() method. */ private startBatchIfNeeded; /** * Records the timestamp when audioVideo.start() was called. */ onStart(): void; /** * Starts a resubscribe signaling timing entry. * Used for mid-meeting resubscribes (e.g. new remote video joins) to capture * the resubscribe latency (subscribe sent → ack → set remote description). * Only the resubscribe-relevant signaling fields are required for completion. */ onResubscribeStart(): void; /** * Indicates that remote video is expected in the current batch. * The batch will not complete until at least one remote video entry * has been added and completed (or the batch times out). * * This method exists because the SDK's initial subscribe does not include * remote video — index ingestion is paused during the first subscribe, * so the downlink policy cannot select video streams until the connection * is established and a second subscribe (resubscribe) is triggered. */ setExpectingRemoteVideo(): void; /** * Clears the expectation that remote video will be part of the current batch. * Called when the downlink policy decides not to subscribe to any video, * so the batch is not held open waiting for remote video that will never arrive. */ clearExpectingRemoteVideo(): void; /** * Records the timestamp when join frame was sent. */ onJoinSent(): void; /** * Records the timestamp when join ack was received. */ onJoinAckReceived(): void; /** * Records the timestamp when signaling WebSocket connection was established. */ onTransportConnected(): void; /** * Records the timestamp when SDP offer was created. */ onCreateOfferCalled(): void; /** * Records the timestamp when local description was set. */ onSetLocalDescription(): void; /** * Records the timestamp when remote description was set. */ onSetRemoteDescription(): void; /** * Records the timestamp when ICE gathering started. */ onIceGatheringStarted(): void; /** * Records the timestamp when ICE gathering completed. */ onIceGatheringComplete(): void; /** * Records the timestamp when ICE connection was established. */ onIceConnected(): void; /** * Records the timestamp when subscribe frame was sent. */ onSubscribeSent(): void; /** * Records the timestamp when subscribe ack was received. */ onSubscribeAckReceived(): void; /** * Records the timestamp when remote audio track was added. */ onRemoteAudioAdded(): void; /** * Records the timestamp when first audio packet was received. */ onRemoteAudioFirstPacketReceived(): void; /** * Records the timestamp when local audio track was added. */ onLocalAudioAdded(): void; /** * Records the timestamp when first audio packet was sent. */ onLocalAudioFirstPacketSent(): void; /** * Records the timestamp when local video track was added. */ onLocalVideoAdded(): void; /** * Records the timestamp when first video frame was sent. */ onLocalVideoFirstFrameSent(): void; /** * Marks local video timing as removed and triggers batch completion check. * This allows the timing data to be emitted with the removed flag. */ onLocalVideoRemoved(): void; /** * Starts tracking timing for a remote video subscription. * Records the added timestamp for the given group_id. * If a timer already exists for this group_id, it is replaced. * * @param groupId The group ID of the remote video subscription */ onRemoteVideoAdded(groupId: number): void; /** * Records that a remote video tile has been bound to a video element. * Only bound remote videos are included in timing emissions. * Unbound remote videos are silently omitted from the batch. * * @param groupId The group ID of the remote video subscription */ onRemoteVideoBound(groupId: number): void; /** * Records that a remote video tile has been unbound from its video element. * The group ID is removed from the bound set so it no longer blocks batch emission. * * @param groupId The group ID of the remote video subscription */ onRemoteVideoUnbound(groupId: number): void; /** * Records the timestamp when first video packet was received for a group_id. * Only the first call for each group_id records the timestamp. * @param groupId The group ID of the remote video subscription */ onRemoteVideoFirstPacketReceived(groupId: number): void; /** * Records the timestamp when first video frame was rendered for a group_id. * @param groupId The group ID of the remote video subscription * @param metadata The VideoFrameCallbackMetadata from requestVideoFrameCallback, if available */ onRemoteVideoFirstFrameRendered(groupId: number, metadata?: VideoFrameCallbackMetadata): void; /** * Marks timing state for a remote video subscription as removed. * The timing data will be emitted with the removed flag. * @param groupId The group ID of the remote video subscription */ onRemoteVideoRemoved(groupId: number): void; /** * Clears all timing state and resets the manager for a new session. * This should be called when starting a new meeting session. */ reset(): void; /** * Stops the timeout check interval and cleans up resources. * This should be called when the meeting session ends. */ destroy(): void; /** * Returns the current timestamp in milliseconds since epoch. */ private getCurrentTimestamp; /** * Checks if signaling timing is complete. * Complete when all signaling timestamps are set. */ private isSignalingComplete; /** * Checks if remote audio timing is complete. * Only checks added_ms && first_packet_received_ms. * first_frame_rendered_ms is optional (not required for completion). */ private isRemoteAudioComplete; /** * Checks if local audio timing is complete. * Only checks added_ms && first_packet_sent_ms. * first_frame_captured_ms is optional (not required for completion). */ private isLocalAudioComplete; /** * Checks if local video timing is complete. * Only checks added_ms && first_frame_sent_ms. * first_frame_captured_ms is optional. */ private isLocalVideoComplete; /** * Checks if a remote video timing entry is complete. * Complete when all three timestamps are set, or when removed. */ private isRemoteVideoComplete; /** * Checks if all batch timings are complete. * Each category is only required if its corresponding on*Added was called. */ private areAllBatchTimingsComplete; /** * Checks if the batch is complete and emits if so. */ private maybeEmitBatch; /** * Builds and notifies the observer with timing data, then clears * the reported state so future events start a fresh batch. */ private emitAndReset; /** * Clears state that was included in the last emission so it is not re-sent. * Resets the batch timer so new events can start a fresh batch. */ private clearReportedState; /** * Builds the MeetingSessionTiming structure from current state. * Per-struct timedOut flags: a struct is timed out if the batch timed out AND that struct is not complete. * @param batchTimedOut Whether the batch timed out */ private buildMeetingSessionTiming; private scheduleBatchTimeout; private cancelBatchTimeout; }