From 18489c1ddeff578701c8464f4822c60396643a96 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 18 Mar 2026 10:59:45 +0100 Subject: [PATCH 1/2] feat(client): echo negotiationId in subscriber offer answer Echo the negotiationId from the SubscriberOffer back in the SendAnswerRequest so the SFU can correlate offers with their corresponding answers. Also includes codegen updates from protocol PR #1717 and populates the new webrtcVersion field in ClientDetails. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../client/src/gen/video/sfu/event/events.ts | 10 + .../client/src/gen/video/sfu/models/models.ts | 338 ++++++++++++++++++ .../gen/video/sfu/signal_rpc/signal.client.ts | 30 +- .../src/gen/video/sfu/signal_rpc/signal.ts | 137 ++++++- packages/client/src/helpers/client-details.ts | 5 +- packages/client/src/rtc/Subscriber.ts | 1 + .../src/rtc/__tests__/Subscriber.test.ts | 6 +- 7 files changed, 508 insertions(+), 19 deletions(-) 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..22b4889111 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,25 @@ + // @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 +95,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 +371,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 +541,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 +871,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 +986,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, }); }); }); From 3b14f181944c78047c9ed7c61c34558a1aa01c36 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 18 Mar 2026 11:15:23 +0100 Subject: [PATCH 2/2] chore: lint fix --- packages/client/src/gen/video/sfu/signal_rpc/signal.ts | 1 - 1 file changed, 1 deletion(-) 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 22b4889111..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,4 +1,3 @@ - // @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