diff --git a/packages/client/src/gen/video/sfu/event/events.ts b/packages/client/src/gen/video/sfu/event/events.ts index 12d102d5cd..37c7e38978 100644 --- a/packages/client/src/gen/video/sfu/event/events.ts +++ b/packages/client/src/gen/video/sfu/event/events.ts @@ -670,6 +670,10 @@ export interface SubscriberOffer { * @generated from protobuf field: string sdp = 2; */ sdp: string; + /** + * @generated from protobuf field: uint32 negotiation_id = 3; + */ + negotiationId: number; } /** * @generated from protobuf message stream.video.sfu.event.PublisherAnswer @@ -1596,6 +1600,12 @@ class SubscriberOffer$Type extends MessageType { super('stream.video.sfu.event.SubscriberOffer', [ { no: 1, name: 'ice_restart', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ }, { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, + { + no: 3, + name: 'negotiation_id', + kind: 'scalar', + T: 13 /*ScalarType.UINT32*/, + }, ]); } } diff --git a/packages/client/src/gen/video/sfu/models/models.ts b/packages/client/src/gen/video/sfu/models/models.ts index aa35bb2639..a5e3b40490 100644 --- a/packages/client/src/gen/video/sfu/models/models.ts +++ b/packages/client/src/gen/video/sfu/models/models.ts @@ -447,6 +447,10 @@ export interface ClientDetails { * @generated from protobuf field: stream.video.sfu.models.Device device = 4; */ device?: Device; + /** + * @generated from protobuf field: string webrtc_version = 5; + */ + webrtcVersion: string; } /** * @generated from protobuf message stream.video.sfu.models.Sdk @@ -700,6 +704,155 @@ export interface PerformanceStats { */ targetBitrate: number; } +/** + * =================================================================== + * BASE (shared by all RTP directions) + * =================================================================== + * + * @generated from protobuf message stream.video.sfu.models.RtpBase + */ +export interface RtpBase { + /** + * @generated from protobuf field: uint32 ssrc = 1; + */ + ssrc: number; // raw stat["ssrc"] + /** + * @generated from protobuf field: string kind = 2; + */ + kind: string; // stat["kind"] ("audio","video") + /** + * @generated from protobuf field: double timestamp_ms = 3; + */ + timestampMs: number; // stat["timestamp"] in milliseconds +} +/** + * =================================================================== + * INBOUND (SUBSCRIBER RECEIVING MEDIA) + * =================================================================== + * + * @generated from protobuf message stream.video.sfu.models.InboundRtp + */ +export interface InboundRtp { + /** + * @generated from protobuf field: stream.video.sfu.models.RtpBase base = 1; + */ + base?: RtpBase; + /** + * @generated from protobuf field: double jitter_seconds = 2; + */ + jitterSeconds: number; // stat["jitter"] + /** + * @generated from protobuf field: uint64 packets_received = 3; + */ + packetsReceived: string; // stat["packetsReceived"] + /** + * @generated from protobuf field: uint64 packets_lost = 4; + */ + packetsLost: string; // stat["packetsLost"] + /** + * @generated from protobuf field: double packet_loss_percent = 5; + */ + packetLossPercent: number; // (packets_lost / (packets_received + packets_lost)) * 100;skip if denominator <= 0 or counters decreased + /** + * -------- AUDIO METRICS -------- + * + * @generated from protobuf field: uint32 concealment_events = 10; + */ + concealmentEvents: number; // stat["concealmentEvents"] + /** + * @generated from protobuf field: double concealment_percent = 11; + */ + concealmentPercent: number; // (concealedSamples / totalSamplesReceived) * 100 when totalSamplesReceived >= 96_000 (≈2 s @ 48 kHz) + /** + * -------- VIDEO METRICS -------- + * + * @generated from protobuf field: double fps = 20; + */ + fps: number; // use delta(framesDecoded)/delta(time) with prev sample + /** + * @generated from protobuf field: double freeze_duration_seconds = 21; + */ + freezeDurationSeconds: number; // stat["totalFreezesDuration"] + /** + * @generated from protobuf field: double avg_decode_time_seconds = 22; + */ + avgDecodeTimeSeconds: number; // stat["totalDecodeTime"] / max(1, stat["framesDecoded"]) + /** + * @generated from protobuf field: uint32 min_dimension_px = 23; + */ + minDimensionPx: number; // min(stat["frameWidth"], stat["frameHeight"]) for video-like tracks +} +/** + * =================================================================== + * OUTBOUND (PUBLISHER SENDING MEDIA) + * =================================================================== + * + * @generated from protobuf message stream.video.sfu.models.OutboundRtp + */ +export interface OutboundRtp { + /** + * @generated from protobuf field: stream.video.sfu.models.RtpBase base = 1; + */ + base?: RtpBase; + /** + * @generated from protobuf field: double fps = 10; + */ + fps: number; // delta(framesEncoded)/delta(time) if missing + /** + * @generated from protobuf field: double avg_encode_time_seconds = 11; + */ + avgEncodeTimeSeconds: number; // stat["totalEncodeTime"] / max(1, stat["framesEncoded"]) + /** + * @generated from protobuf field: double bitrate_bps = 12; + */ + bitrateBps: number; // delta(bytesSent)*8 / delta(timeSeconds); requires prev bytes/timestamp; ignore if delta<=0 + /** + * @generated from protobuf field: uint32 min_dimension_px = 13; + */ + minDimensionPx: number; // min(stat["frameWidth"], stat["frameHeight"]) +} +/** + * =================================================================== + * SFU FEEDBACK: REMOTE-INBOUND (Publisher receives feedback) + * =================================================================== + * + * @generated from protobuf message stream.video.sfu.models.RemoteInboundRtp + */ +export interface RemoteInboundRtp { + /** + * @generated from protobuf field: stream.video.sfu.models.RtpBase base = 1; + */ + base?: RtpBase; + /** + * @generated from protobuf field: double jitter_seconds = 2; + */ + jitterSeconds: number; // stat["jitter"] + /** + * @generated from protobuf field: double round_trip_time_s = 3; + */ + roundTripTimeS: number; // stat["roundTripTime"] +} +/** + * =================================================================== + * SFU FEEDBACK: REMOTE-OUTBOUND (Subscriber receives feedback) + * =================================================================== + * + * @generated from protobuf message stream.video.sfu.models.RemoteOutboundRtp + */ +export interface RemoteOutboundRtp { + /** + * @generated from protobuf field: stream.video.sfu.models.RtpBase base = 1; + */ + base?: RtpBase; + /** + * @generated from protobuf field: double jitter_seconds = 2; + */ + jitterSeconds: number; // stat["jitter"] if provided + /** + * @generated from protobuf field: double round_trip_time_s = 3; + */ + roundTripTimeS: number; // stat["roundTripTime"] +} /** * @generated from protobuf enum stream.video.sfu.models.PeerType */ @@ -967,6 +1120,14 @@ export enum SdkType { * @generated from protobuf enum value: SDK_TYPE_PLAIN_JAVASCRIPT = 9; */ PLAIN_JAVASCRIPT = 9, + /** + * @generated from protobuf enum value: SDK_TYPE_PYTHON = 10; + */ + PYTHON = 10, + /** + * @generated from protobuf enum value: SDK_TYPE_VISION_AGENTS = 11; + */ + VISION_AGENTS = 11, } /** * @generated from protobuf enum stream.video.sfu.models.TrackUnpublishReason @@ -1171,6 +1332,12 @@ export enum ClientCapability { * @generated from protobuf enum value: CLIENT_CAPABILITY_SUBSCRIBER_VIDEO_PAUSE = 1; */ SUBSCRIBER_VIDEO_PAUSE = 1, + /** + * Instructs SFU that stats will be sent to the coordinator + * + * @generated from protobuf enum value: CLIENT_CAPABILITY_COORDINATOR_STATS = 2; + */ + COORDINATOR_STATS = 2, } // @generated message type with reflection information, may provide speed optimized methods class CallState$Type extends MessageType { @@ -1597,6 +1764,12 @@ class ClientDetails$Type extends MessageType { { no: 2, name: 'os', kind: 'message', T: () => OS }, { no: 3, name: 'browser', kind: 'message', T: () => Browser }, { no: 4, name: 'device', kind: 'message', T: () => Device }, + { + no: 5, + name: 'webrtc_version', + kind: 'scalar', + T: 9 /*ScalarType.STRING*/, + }, ]); } } @@ -1869,3 +2042,168 @@ class PerformanceStats$Type extends MessageType { * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats */ export const PerformanceStats = new PerformanceStats$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RtpBase$Type extends MessageType { + constructor() { + super('stream.video.sfu.models.RtpBase', [ + { no: 1, name: 'ssrc', kind: 'scalar', T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: 'kind', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, + { + no: 3, + name: 'timestamp_ms', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.models.RtpBase + */ +export const RtpBase = new RtpBase$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class InboundRtp$Type extends MessageType { + constructor() { + super('stream.video.sfu.models.InboundRtp', [ + { no: 1, name: 'base', kind: 'message', T: () => RtpBase }, + { + no: 2, + name: 'jitter_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 3, + name: 'packets_received', + kind: 'scalar', + T: 4 /*ScalarType.UINT64*/, + }, + { + no: 4, + name: 'packets_lost', + kind: 'scalar', + T: 4 /*ScalarType.UINT64*/, + }, + { + no: 5, + name: 'packet_loss_percent', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 10, + name: 'concealment_events', + kind: 'scalar', + T: 13 /*ScalarType.UINT32*/, + }, + { + no: 11, + name: 'concealment_percent', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { no: 20, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ }, + { + no: 21, + name: 'freeze_duration_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 22, + name: 'avg_decode_time_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 23, + name: 'min_dimension_px', + kind: 'scalar', + T: 13 /*ScalarType.UINT32*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.models.InboundRtp + */ +export const InboundRtp = new InboundRtp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class OutboundRtp$Type extends MessageType { + constructor() { + super('stream.video.sfu.models.OutboundRtp', [ + { no: 1, name: 'base', kind: 'message', T: () => RtpBase }, + { no: 10, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ }, + { + no: 11, + name: 'avg_encode_time_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 12, + name: 'bitrate_bps', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 13, + name: 'min_dimension_px', + kind: 'scalar', + T: 13 /*ScalarType.UINT32*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.models.OutboundRtp + */ +export const OutboundRtp = new OutboundRtp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RemoteInboundRtp$Type extends MessageType { + constructor() { + super('stream.video.sfu.models.RemoteInboundRtp', [ + { no: 1, name: 'base', kind: 'message', T: () => RtpBase }, + { + no: 2, + name: 'jitter_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 3, + name: 'round_trip_time_s', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.models.RemoteInboundRtp + */ +export const RemoteInboundRtp = new RemoteInboundRtp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class RemoteOutboundRtp$Type extends MessageType { + constructor() { + super('stream.video.sfu.models.RemoteOutboundRtp', [ + { no: 1, name: 'base', kind: 'message', T: () => RtpBase }, + { + no: 2, + name: 'jitter_seconds', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + { + no: 3, + name: 'round_trip_time_s', + kind: 'scalar', + T: 1 /*ScalarType.DOUBLE*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.models.RemoteOutboundRtp + */ +export const RemoteOutboundRtp = new RemoteOutboundRtp$Type(); diff --git a/packages/client/src/gen/video/sfu/signal_rpc/signal.client.ts b/packages/client/src/gen/video/sfu/signal_rpc/signal.client.ts index 799f1c839c..74cb828d90 100644 --- a/packages/client/src/gen/video/sfu/signal_rpc/signal.client.ts +++ b/packages/client/src/gen/video/sfu/signal_rpc/signal.client.ts @@ -14,6 +14,8 @@ import type { ICETrickleResponse, SendAnswerRequest, SendAnswerResponse, + SendMetricsRequest, + SendMetricsResponse, SendStatsRequest, SendStatsResponse, SetPublisherRequest, @@ -92,6 +94,13 @@ export interface ISignalServerClient { input: SendStatsRequest, options?: RpcOptions, ): UnaryCall; + /** + * @generated from protobuf rpc: SendMetrics(stream.video.sfu.signal.SendMetricsRequest) returns (stream.video.sfu.signal.SendMetricsResponse); + */ + sendMetrics( + input: SendMetricsRequest, + options?: RpcOptions, + ): UnaryCall; /** * @generated from protobuf rpc: StartNoiseCancellation(stream.video.sfu.signal.StartNoiseCancellationRequest) returns (stream.video.sfu.signal.StartNoiseCancellationResponse); */ @@ -240,6 +249,23 @@ export class SignalServerClient implements ISignalServerClient, ServiceInfo { input, ); } + /** + * @generated from protobuf rpc: SendMetrics(stream.video.sfu.signal.SendMetricsRequest) returns (stream.video.sfu.signal.SendMetricsResponse); + */ + sendMetrics( + input: SendMetricsRequest, + options?: RpcOptions, + ): UnaryCall { + const method = this.methods[7], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'unary', + this._transport, + method, + opt, + input, + ); + } /** * @generated from protobuf rpc: StartNoiseCancellation(stream.video.sfu.signal.StartNoiseCancellationRequest) returns (stream.video.sfu.signal.StartNoiseCancellationResponse); */ @@ -247,7 +273,7 @@ export class SignalServerClient implements ISignalServerClient, ServiceInfo { input: StartNoiseCancellationRequest, options?: RpcOptions, ): UnaryCall { - const method = this.methods[7], + const method = this.methods[8], opt = this._transport.mergeOptions(options); return stackIntercept< StartNoiseCancellationRequest, @@ -261,7 +287,7 @@ export class SignalServerClient implements ISignalServerClient, ServiceInfo { input: StopNoiseCancellationRequest, options?: RpcOptions, ): UnaryCall { - const method = this.methods[8], + const method = this.methods[9], opt = this._transport.mergeOptions(options); return stackIntercept< StopNoiseCancellationRequest, diff --git a/packages/client/src/gen/video/sfu/signal_rpc/signal.ts b/packages/client/src/gen/video/sfu/signal_rpc/signal.ts index 84c11a0402..e7b7c6cf70 100644 --- a/packages/client/src/gen/video/sfu/signal_rpc/signal.ts +++ b/packages/client/src/gen/video/sfu/signal_rpc/signal.ts @@ -1,23 +1,24 @@ // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size // @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3) // tslint:disable -import { - AndroidState, - AppleState, - Error, - ICETrickle, - InputDevices, - PeerType, - PerformanceStats, - RTMPIngress, - TrackInfo, - TrackType, - VideoDimension, - WebsocketReconnectStrategy, -} from '../models/models'; +import { ICETrickle } from '../models/models'; import { ServiceType } from '@protobuf-ts/runtime-rpc'; import { MessageType } from '@protobuf-ts/runtime'; - +import { TrackInfo } from '../models/models'; +import { VideoDimension } from '../models/models'; +import { TrackType } from '../models/models'; +import { PeerType } from '../models/models'; +import { PerformanceStats } from '../models/models'; +import { RTMPIngress } from '../models/models'; +import { AppleState } from '../models/models'; +import { AndroidState } from '../models/models'; +import { InputDevices } from '../models/models'; +import { RemoteOutboundRtp } from '../models/models'; +import { RemoteInboundRtp } from '../models/models'; +import { OutboundRtp } from '../models/models'; +import { InboundRtp } from '../models/models'; +import { WebsocketReconnectStrategy } from '../models/models'; +import { Error } from '../models/models'; /** * @generated from protobuf message stream.video.sfu.signal.StartNoiseCancellationRequest */ @@ -93,6 +94,39 @@ export interface Telemetry { oneofKind: undefined; }; } +/** + * @generated from protobuf message stream.video.sfu.signal.SendMetricsRequest + */ +export interface SendMetricsRequest { + /** + * @generated from protobuf field: string session_id = 1; + */ + sessionId: string; + /** + * @generated from protobuf field: string unified_session_id = 2; + */ + unifiedSessionId: string; + /** + * @generated from protobuf field: repeated stream.video.sfu.models.InboundRtp inbounds = 3; + */ + inbounds: InboundRtp[]; + /** + * @generated from protobuf field: repeated stream.video.sfu.models.OutboundRtp outbounds = 4; + */ + outbounds: OutboundRtp[]; + /** + * @generated from protobuf field: repeated stream.video.sfu.models.RemoteInboundRtp remote_inbounds = 5; + */ + remoteInbounds: RemoteInboundRtp[]; + /** + * @generated from protobuf field: repeated stream.video.sfu.models.RemoteOutboundRtp remote_outbounds = 6; + */ + remoteOutbounds: RemoteOutboundRtp[]; +} +/** + * @generated from protobuf message stream.video.sfu.signal.SendMetricsResponse + */ +export interface SendMetricsResponse {} /** * @generated from protobuf message stream.video.sfu.signal.SendStatsRequest */ @@ -336,6 +370,10 @@ export interface SendAnswerRequest { * @generated from protobuf field: string session_id = 3; */ sessionId: string; + /** + * @generated from protobuf field: uint32 negotiation_id = 4; + */ + negotiationId: number; } /** * @generated from protobuf message stream.video.sfu.signal.SendAnswerResponse @@ -502,6 +540,62 @@ class Telemetry$Type extends MessageType { */ export const Telemetry = new Telemetry$Type(); // @generated message type with reflection information, may provide speed optimized methods +class SendMetricsRequest$Type extends MessageType { + constructor() { + super('stream.video.sfu.signal.SendMetricsRequest', [ + { no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, + { + no: 2, + name: 'unified_session_id', + kind: 'scalar', + T: 9 /*ScalarType.STRING*/, + }, + { + no: 3, + name: 'inbounds', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => InboundRtp, + }, + { + no: 4, + name: 'outbounds', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => OutboundRtp, + }, + { + no: 5, + name: 'remote_inbounds', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => RemoteInboundRtp, + }, + { + no: 6, + name: 'remote_outbounds', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => RemoteOutboundRtp, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsRequest + */ +export const SendMetricsRequest = new SendMetricsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SendMetricsResponse$Type extends MessageType { + constructor() { + super('stream.video.sfu.signal.SendMetricsResponse', []); + } +} +/** + * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsResponse + */ +export const SendMetricsResponse = new SendMetricsResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods class SendStatsRequest$Type extends MessageType { constructor() { super('stream.video.sfu.signal.SendStatsRequest', [ @@ -776,6 +870,12 @@ class SendAnswerRequest$Type extends MessageType { }, { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, { no: 3, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, + { + no: 4, + name: 'negotiation_id', + kind: 'scalar', + T: 13 /*ScalarType.UINT32*/, + }, ]); } } @@ -885,6 +985,12 @@ export const SignalServer = new ServiceType( I: SendStatsRequest, O: SendStatsResponse, }, + { + name: 'SendMetrics', + options: {}, + I: SendMetricsRequest, + O: SendMetricsResponse, + }, { name: 'StartNoiseCancellation', options: {}, diff --git a/packages/client/src/helpers/client-details.ts b/packages/client/src/helpers/client-details.ts index 14be7278f8..e963c6bcb0 100644 --- a/packages/client/src/helpers/client-details.ts +++ b/packages/client/src/helpers/client-details.ts @@ -137,6 +137,7 @@ export const getClientDetails = async (): Promise => { sdk: sdkInfo, os: osInfo, device: deviceInfo, + webrtcVersion: webRtcInfo?.version || '', }; } @@ -173,11 +174,12 @@ export const getClientDetails = async (): Promise => { const uaBrowser = userAgentData?.fullVersionList?.find( (v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g), ); + const browserVersion = uaBrowser?.version || browser.version || ''; return { sdk: sdkInfo, browser: { name: uaBrowser?.brand || browser.name || navigator.userAgent, - version: uaBrowser?.version || browser.version || '', + version: browserVersion, }, os: { name: userAgentData?.platform || os.name || '', @@ -190,5 +192,6 @@ export const getClientDetails = async (): Promise => { .join(' '), version: '', }, + webrtcVersion: browserVersion, }; }; diff --git a/packages/client/src/rtc/Subscriber.ts b/packages/client/src/rtc/Subscriber.ts index ed6c01824e..7fae8b2733 100644 --- a/packages/client/src/rtc/Subscriber.ts +++ b/packages/client/src/rtc/Subscriber.ts @@ -168,6 +168,7 @@ export class Subscriber extends BasePeerConnection { await this.sfuClient.sendAnswer({ peerType: PeerType.SUBSCRIBER, sdp: answer.sdp || '', + negotiationId: subscriberOffer.negotiationId, }); this.isIceRestarting = false; diff --git a/packages/client/src/rtc/__tests__/Subscriber.test.ts b/packages/client/src/rtc/__tests__/Subscriber.test.ts index 1a4f9e3781..71bf540a57 100644 --- a/packages/client/src/rtc/__tests__/Subscriber.test.ts +++ b/packages/client/src/rtc/__tests__/Subscriber.test.ts @@ -224,7 +224,10 @@ describe('Subscriber', () => { .mockResolvedValue({ sdp: 'answer-sdp' }); vi.spyOn(subscriber['pc'], 'setRemoteDescription').mockResolvedValue(); - const offer = SubscriberOffer.create({ sdp: 'offer-sdp' }); + const offer = SubscriberOffer.create({ + sdp: 'offer-sdp', + negotiationId: 42, + }); // @ts-expect-error - private method await subscriber.negotiate(offer); expect(subscriber['pc'].setRemoteDescription).toHaveBeenCalledWith({ @@ -236,6 +239,7 @@ describe('Subscriber', () => { expect(sfuClient.sendAnswer).toHaveBeenCalledWith({ peerType: PeerType.SUBSCRIBER, sdp: 'answer-sdp', + negotiationId: 42, }); }); });