@tx5dr/contracts 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/schema/__tests__/radio-power-support.test.d.ts +2 -0
  2. package/dist/schema/__tests__/radio-power-support.test.d.ts.map +1 -0
  3. package/dist/schema/__tests__/radio-power-support.test.js +19 -0
  4. package/dist/schema/__tests__/radio-power-support.test.js.map +1 -0
  5. package/dist/schema/__tests__/realtime-settings.schema.test.d.ts +2 -0
  6. package/dist/schema/__tests__/realtime-settings.schema.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/realtime-settings.schema.test.js +122 -0
  8. package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -0
  9. package/dist/schema/cpu-profile.schema.d.ts +4 -4
  10. package/dist/schema/desktop-https.schema.d.ts +9 -0
  11. package/dist/schema/desktop-https.schema.d.ts.map +1 -1
  12. package/dist/schema/desktop-https.schema.js +3 -0
  13. package/dist/schema/desktop-https.schema.js.map +1 -1
  14. package/dist/schema/openwebrx.schema.d.ts +1 -1
  15. package/dist/schema/openwebrx.schema.d.ts.map +1 -1
  16. package/dist/schema/openwebrx.schema.js +1 -1
  17. package/dist/schema/openwebrx.schema.js.map +1 -1
  18. package/dist/schema/operator.schema.d.ts +32 -32
  19. package/dist/schema/plugin.schema.d.ts +702 -168
  20. package/dist/schema/plugin.schema.d.ts.map +1 -1
  21. package/dist/schema/plugin.schema.js +38 -3
  22. package/dist/schema/plugin.schema.js.map +1 -1
  23. package/dist/schema/radio-capability.schema.d.ts +42 -2
  24. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  25. package/dist/schema/radio-capability.schema.js +18 -1
  26. package/dist/schema/radio-capability.schema.js.map +1 -1
  27. package/dist/schema/radio-power-support.d.ts +8 -0
  28. package/dist/schema/radio-power-support.d.ts.map +1 -1
  29. package/dist/schema/radio-power-support.js +38 -26
  30. package/dist/schema/radio-power-support.js.map +1 -1
  31. package/dist/schema/radio-power.schema.d.ts +10 -6
  32. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  33. package/dist/schema/radio-power.schema.js +8 -6
  34. package/dist/schema/radio-power.schema.js.map +1 -1
  35. package/dist/schema/realtime.schema.d.ts +797 -417
  36. package/dist/schema/realtime.schema.d.ts.map +1 -1
  37. package/dist/schema/realtime.schema.js +205 -51
  38. package/dist/schema/realtime.schema.js.map +1 -1
  39. package/dist/schema/server-message-keys.d.ts +2 -2
  40. package/dist/schema/server-message-keys.d.ts.map +1 -1
  41. package/dist/schema/server-message-keys.js +2 -2
  42. package/dist/schema/server-message-keys.js.map +1 -1
  43. package/dist/schema/slot-info.schema.d.ts +6 -0
  44. package/dist/schema/slot-info.schema.d.ts.map +1 -1
  45. package/dist/schema/slot-info.schema.js +3 -1
  46. package/dist/schema/slot-info.schema.js.map +1 -1
  47. package/dist/schema/websocket.schema.d.ts +1436 -640
  48. package/dist/schema/websocket.schema.d.ts.map +1 -1
  49. package/dist/schema/websocket.schema.js +2 -1
  50. package/dist/schema/websocket.schema.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/schema/__tests__/radio-power-support.test.ts +26 -0
  53. package/src/schema/__tests__/realtime-settings.schema.test.ts +142 -0
  54. package/src/schema/desktop-https.schema.ts +3 -1
  55. package/src/schema/openwebrx.schema.ts +1 -1
  56. package/src/schema/plugin.schema.ts +44 -3
  57. package/src/schema/radio-capability.schema.ts +25 -1
  58. package/src/schema/radio-power-support.ts +49 -26
  59. package/src/schema/radio-power.schema.ts +8 -6
  60. package/src/schema/realtime.schema.ts +247 -65
  61. package/src/schema/server-message-keys.ts +2 -2
  62. package/src/schema/slot-info.schema.ts +3 -1
  63. package/src/schema/websocket.schema.ts +3 -1
@@ -3,15 +3,158 @@ import { z } from 'zod';
3
3
  export const RealtimeScopeSchema = z.enum(['radio', 'openwebrx-preview']);
4
4
  export type RealtimeScope = z.infer<typeof RealtimeScopeSchema>;
5
5
 
6
- export const RealtimeParticipantKindSchema = z.enum(['listener', 'publisher', 'bridge']);
7
- export type RealtimeParticipantKind = z.infer<typeof RealtimeParticipantKindSchema>;
8
-
9
- export const RealtimeTransportKindSchema = z.enum(['livekit', 'ws-compat']);
6
+ export const RealtimeTransportKindSchema = z.enum(['rtc-data-audio', 'ws-compat']);
10
7
  export type RealtimeTransportKind = z.infer<typeof RealtimeTransportKindSchema>;
11
8
 
12
9
  export const RealtimeSessionDirectionSchema = z.enum(['recv', 'send']);
13
10
  export type RealtimeSessionDirection = z.infer<typeof RealtimeSessionDirectionSchema>;
14
11
 
12
+ export const RealtimeAudioCodecSchema = z.enum(['opus', 'pcm-s16le']);
13
+ export type RealtimeAudioCodec = z.infer<typeof RealtimeAudioCodecSchema>;
14
+
15
+ export const RealtimeAudioCodecPreferenceSchema = z.enum(['auto', 'opus', 'pcm']);
16
+ export type RealtimeAudioCodecPreference = z.infer<typeof RealtimeAudioCodecPreferenceSchema>;
17
+
18
+ export const DEFAULT_REALTIME_AUDIO_CODEC_PREFERENCE: RealtimeAudioCodecPreference = 'auto';
19
+
20
+ export const RealtimeAudioCodecCapabilitiesSchema = z.object({
21
+ opus: z.object({
22
+ encode: z.boolean().optional(),
23
+ decode: z.boolean().optional(),
24
+ sampleRates: z.array(z.number().int().positive()).optional(),
25
+ encodeSampleRates: z.array(z.number().int().positive()).optional(),
26
+ decodeSampleRates: z.array(z.number().int().positive()).optional(),
27
+ }).optional(),
28
+ pcmS16le: z.boolean().optional(),
29
+ }).optional();
30
+ export type RealtimeAudioCodecCapabilities = z.infer<typeof RealtimeAudioCodecCapabilitiesSchema>;
31
+
32
+ export const ResolvedRealtimeAudioCodecPolicySchema = z.object({
33
+ preference: RealtimeAudioCodecPreferenceSchema,
34
+ resolvedCodec: RealtimeAudioCodecSchema,
35
+ fallbackReason: z.enum([
36
+ 'not-needed',
37
+ 'client-forced-pcm',
38
+ 'client-opus-unavailable',
39
+ 'server-opus-unavailable',
40
+ 'scope-not-supported',
41
+ ]).nullable(),
42
+ codecSampleRate: z.number().int().positive().nullable(),
43
+ bitrateBps: z.number().int().positive().nullable(),
44
+ frameDurationMs: z.number().int().positive().nullable(),
45
+ });
46
+ export type ResolvedRealtimeAudioCodecPolicy = z.infer<typeof ResolvedRealtimeAudioCodecPolicySchema>;
47
+
48
+ export const VoiceTxBufferProfileSchema = z.enum(['auto', 'custom']);
49
+ export type VoiceTxBufferProfile = z.infer<typeof VoiceTxBufferProfileSchema>;
50
+
51
+ export const VoiceTxCustomTargetBufferMsSchema = z.preprocess((value) => {
52
+ if (value === '' || value === undefined || value === null) {
53
+ return undefined;
54
+ }
55
+ if (typeof value === 'string') {
56
+ const parsed = Number.parseInt(value, 10);
57
+ return Number.isFinite(parsed) ? parsed : value;
58
+ }
59
+ return value;
60
+ }, z.number().int().min(40).max(500).optional());
61
+
62
+ export const VoiceTxBufferPreferenceSchema = z.preprocess((value) => {
63
+ if (!value || typeof value !== 'object') {
64
+ return value;
65
+ }
66
+ const preference = value as { profile?: unknown };
67
+ if (
68
+ preference.profile === 'low-latency'
69
+ || preference.profile === 'balanced'
70
+ || preference.profile === 'stable'
71
+ ) {
72
+ return { ...preference, profile: 'auto' };
73
+ }
74
+ return value;
75
+ }, z.object({
76
+ profile: VoiceTxBufferProfileSchema.default('auto'),
77
+ customTargetBufferMs: VoiceTxCustomTargetBufferMsSchema,
78
+ })).superRefine((value, ctx) => {
79
+ if (value.profile === 'custom' && typeof value.customTargetBufferMs !== 'number') {
80
+ ctx.addIssue({
81
+ code: z.ZodIssueCode.custom,
82
+ path: ['customTargetBufferMs'],
83
+ message: 'Custom TX buffer target is required for custom profile',
84
+ });
85
+ }
86
+ });
87
+ export type VoiceTxBufferPreference = z.infer<typeof VoiceTxBufferPreferenceSchema>;
88
+
89
+ export const ResolvedVoiceTxBufferPolicySchema = z.object({
90
+ profile: VoiceTxBufferProfileSchema,
91
+ targetMs: z.number().int().min(1),
92
+ minMs: z.number().int().min(1),
93
+ maxMs: z.number().int().min(1),
94
+ headroomMs: z.number().int().min(1),
95
+ staleFrameMs: z.number().int().min(1),
96
+ uplinkMaxBufferedAudioMs: z.number().int().min(1),
97
+ uplinkDegradedBufferedAudioMs: z.number().int().min(1),
98
+ });
99
+ export type ResolvedVoiceTxBufferPolicy = z.infer<typeof ResolvedVoiceTxBufferPolicySchema>;
100
+
101
+ export const DEFAULT_VOICE_TX_BUFFER_PROFILE: VoiceTxBufferProfile = 'auto';
102
+
103
+ const VOICE_TX_BUFFER_PRESETS: Record<Exclude<VoiceTxBufferProfile, 'custom'>, ResolvedVoiceTxBufferPolicy> = {
104
+ auto: {
105
+ profile: 'auto',
106
+ targetMs: 80,
107
+ minMs: 60,
108
+ maxMs: 400,
109
+ headroomMs: 40,
110
+ staleFrameMs: 650,
111
+ uplinkMaxBufferedAudioMs: 180,
112
+ uplinkDegradedBufferedAudioMs: 360,
113
+ },
114
+ };
115
+
116
+ function clampVoiceTxBufferValue(value: number, min: number, max: number): number {
117
+ return Math.max(min, Math.min(max, Math.round(value)));
118
+ }
119
+
120
+ export function resolveVoiceTxBufferPolicy(
121
+ preference?: VoiceTxBufferPreference | null,
122
+ ): ResolvedVoiceTxBufferPolicy {
123
+ const parsedPreference = preference
124
+ ? VoiceTxBufferPreferenceSchema.safeParse(preference)
125
+ : null;
126
+ const normalizedPreference = parsedPreference?.success
127
+ ? parsedPreference.data
128
+ : { profile: DEFAULT_VOICE_TX_BUFFER_PROFILE as VoiceTxBufferProfile };
129
+
130
+ if (normalizedPreference.profile !== 'custom') {
131
+ return { ...VOICE_TX_BUFFER_PRESETS[normalizedPreference.profile] };
132
+ }
133
+
134
+ const targetMs = clampVoiceTxBufferValue(normalizedPreference.customTargetBufferMs ?? 90, 40, 500);
135
+ const minMs = clampVoiceTxBufferValue(targetMs * 0.65, 30, targetMs);
136
+ const maxMs = clampVoiceTxBufferValue(targetMs * 2.5, targetMs + 40, 900);
137
+ const headroomMs = clampVoiceTxBufferValue(targetMs * 0.45, 20, 180);
138
+ const staleFrameMs = clampVoiceTxBufferValue(targetMs * 5, maxMs + 80, 1800);
139
+ const uplinkMaxBufferedAudioMs = clampVoiceTxBufferValue(targetMs * 2, 80, 1000);
140
+ const uplinkDegradedBufferedAudioMs = clampVoiceTxBufferValue(
141
+ targetMs * 4,
142
+ uplinkMaxBufferedAudioMs + 60,
143
+ 2000,
144
+ );
145
+
146
+ return {
147
+ profile: 'custom',
148
+ targetMs,
149
+ minMs,
150
+ maxMs,
151
+ headroomMs,
152
+ staleFrameMs,
153
+ uplinkMaxBufferedAudioMs,
154
+ uplinkDegradedBufferedAudioMs,
155
+ };
156
+ }
157
+
15
158
  export const RealtimeConnectivityErrorCodeSchema = z.enum([
16
159
  'TOKEN_REQUEST_FAILED',
17
160
  'SIGNALING_UNREACHABLE',
@@ -27,10 +170,14 @@ export type RealtimeConnectivityErrorCode = z.infer<typeof RealtimeConnectivityE
27
170
 
28
171
  export const RealtimeConnectivityHintsSchema = z.object({
29
172
  signalingUrl: z.string(),
30
- signalingPort: z.number().int().positive(),
31
- rtcTcpPort: z.number().int().positive(),
32
- udpPortRange: z.string(),
33
- publicUrlOverrideActive: z.boolean(),
173
+ localUdpPort: z.number().int().min(1).max(65535),
174
+ publicCandidateEnabled: z.boolean(),
175
+ publicEndpoint: z.object({
176
+ host: z.string(),
177
+ port: z.number().int().min(1).max(65535),
178
+ }).nullable(),
179
+ iceServers: z.array(z.string()),
180
+ fallbackTransport: z.literal('ws-compat'),
34
181
  });
35
182
  export type RealtimeConnectivityHints = z.infer<typeof RealtimeConnectivityHintsSchema>;
36
183
 
@@ -45,47 +192,18 @@ export const RealtimeConnectivityIssueSchema = z.object({
45
192
  });
46
193
  export type RealtimeConnectivityIssue = z.infer<typeof RealtimeConnectivityIssueSchema>;
47
194
 
48
- export const RealtimeTokenRequestSchema = z.object({
49
- scope: RealtimeScopeSchema,
50
- publish: z.boolean().optional(),
51
- previewSessionId: z.string().optional(),
52
- });
53
-
54
- export type RealtimeTokenRequest = z.infer<typeof RealtimeTokenRequestSchema>;
55
-
56
195
  export const RealtimeSessionRequestSchema = z.object({
57
196
  scope: RealtimeScopeSchema,
58
197
  direction: RealtimeSessionDirectionSchema,
59
198
  previewSessionId: z.string().optional(),
60
199
  transportOverride: RealtimeTransportKindSchema.optional(),
200
+ voiceTxBufferPreference: VoiceTxBufferPreferenceSchema.optional(),
201
+ audioCodecPreference: RealtimeAudioCodecPreferenceSchema.default(DEFAULT_REALTIME_AUDIO_CODEC_PREFERENCE),
202
+ audioCodecCapabilities: RealtimeAudioCodecCapabilitiesSchema,
61
203
  });
62
204
 
63
205
  export type RealtimeSessionRequest = z.infer<typeof RealtimeSessionRequestSchema>;
64
206
 
65
- export const RealtimeParticipantMetadataSchema = z.object({
66
- role: z.string(),
67
- tokenId: z.string().nullable().optional(),
68
- operatorIds: z.array(z.string()).optional(),
69
- clientKind: z.string(),
70
- participantKind: RealtimeParticipantKindSchema,
71
- scope: RealtimeScopeSchema,
72
- previewSessionId: z.string().optional(),
73
- });
74
-
75
- export type RealtimeParticipantMetadata = z.infer<typeof RealtimeParticipantMetadataSchema>;
76
-
77
- export const RealtimeTokenResponseSchema = z.object({
78
- url: z.string(),
79
- roomName: z.string(),
80
- token: z.string(),
81
- participantIdentity: z.string(),
82
- participantName: z.string(),
83
- participantMetadata: RealtimeParticipantMetadataSchema,
84
- connectivityHints: RealtimeConnectivityHintsSchema,
85
- });
86
-
87
- export type RealtimeTokenResponse = z.infer<typeof RealtimeTokenResponseSchema>;
88
-
89
207
  export const RealtimeTransportOfferSchema = z.object({
90
208
  transport: RealtimeTransportKindSchema,
91
209
  direction: RealtimeSessionDirectionSchema,
@@ -93,7 +211,6 @@ export const RealtimeTransportOfferSchema = z.object({
93
211
  token: z.string(),
94
212
  participantIdentity: z.string().nullable().optional(),
95
213
  participantName: z.string().nullable().optional(),
96
- roomName: z.string().nullable().optional(),
97
214
  });
98
215
 
99
216
  export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema>;
@@ -101,8 +218,74 @@ export type RealtimeTransportOffer = z.infer<typeof RealtimeTransportOfferSchema
101
218
  export const RealtimeTransportPolicySchema = z.enum(['auto', 'force-compat']);
102
219
  export type RealtimeTransportPolicy = z.infer<typeof RealtimeTransportPolicySchema>;
103
220
 
104
- export const LiveKitNetworkModeSchema = z.enum(['lan', 'internet-auto', 'internet-manual']);
105
- export type LiveKitNetworkMode = z.infer<typeof LiveKitNetworkModeSchema>;
221
+ function isValidIpv4Literal(value: string): boolean {
222
+ const parts = value.split('.');
223
+ return parts.length === 4 && parts.every((part) => {
224
+ if (!/^\d{1,3}$/.test(part)) {
225
+ return false;
226
+ }
227
+ const parsed = Number.parseInt(part, 10);
228
+ return parsed >= 0 && parsed <= 255;
229
+ });
230
+ }
231
+
232
+ function isValidIpv6Literal(value: string): boolean {
233
+ if (!value.includes(':') || value.includes('[') || value.includes(']')) {
234
+ return false;
235
+ }
236
+ try {
237
+ // WHATWG URL parsing gives us a portable IPv6 sanity check without Node-only APIs.
238
+ // The schema still forbids bracket notation because the setting is host-only.
239
+ const parsed = new URL(`http://[${value}]/`);
240
+ return parsed.hostname.length > 2;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ function isValidDnsHostname(value: string): boolean {
247
+ if (value.length > 253 || value.endsWith('.')) {
248
+ return false;
249
+ }
250
+ const labels = value.split('.');
251
+ return labels.every((label) => (
252
+ label.length >= 1
253
+ && label.length <= 63
254
+ && /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)
255
+ ));
256
+ }
257
+
258
+ function isValidRtcDataAudioPublicHost(value: string): boolean {
259
+ const host = value.trim();
260
+ if (!host || /\s/.test(host) || /[/?#@]/.test(host) || /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(host)) {
261
+ return false;
262
+ }
263
+ return isValidIpv4Literal(host) || isValidIpv6Literal(host) || isValidDnsHostname(host);
264
+ }
265
+
266
+ export const RtcDataAudioPublicHostSchema = z.preprocess(
267
+ (value) => (typeof value === 'string' ? value.trim() : value),
268
+ z.union([
269
+ z.literal('').transform(() => null),
270
+ z.string().min(1).max(253).refine(isValidRtcDataAudioPublicHost, {
271
+ message: 'Public host must be an IP address or DNS hostname without scheme, port, or path',
272
+ }),
273
+ z.null(),
274
+ ]),
275
+ );
276
+ export type RtcDataAudioPublicHost = z.infer<typeof RtcDataAudioPublicHostSchema>;
277
+
278
+ export const RtcDataAudioPublicUdpPortSchema = z.preprocess((value) => {
279
+ if (value === '' || value === undefined) {
280
+ return null;
281
+ }
282
+ if (typeof value === 'string') {
283
+ const parsed = Number.parseInt(value, 10);
284
+ return Number.isFinite(parsed) ? parsed : value;
285
+ }
286
+ return value;
287
+ }, z.number().int().min(1).max(65535).nullable());
288
+ export type RtcDataAudioPublicUdpPort = z.infer<typeof RtcDataAudioPublicUdpPortSchema>;
106
289
 
107
290
  export const RealtimeSessionResponseSchema = z.object({
108
291
  scope: RealtimeScopeSchema,
@@ -112,47 +295,40 @@ export const RealtimeSessionResponseSchema = z.object({
112
295
  selectionReason: z.enum([
113
296
  'client-override',
114
297
  'server-policy',
115
- 'bridge-unhealthy',
116
- 'livekit-disabled',
117
- 'runtime-unavailable',
118
- 'default-livekit',
298
+ 'default-rtc-data-audio',
299
+ 'rtc-data-audio-unavailable',
119
300
  ]),
120
301
  forcedCompatibilityMode: z.boolean(),
121
302
  offers: z.array(RealtimeTransportOfferSchema).min(1),
122
303
  connectivityHints: RealtimeConnectivityHintsSchema,
304
+ voiceTxBufferPolicy: ResolvedVoiceTxBufferPolicySchema.optional(),
305
+ audioCodecPolicy: ResolvedRealtimeAudioCodecPolicySchema,
123
306
  });
124
307
 
125
308
  export type RealtimeSessionResponse = z.infer<typeof RealtimeSessionResponseSchema>;
126
309
 
127
310
  export const RealtimeSettingsSchema = z.object({
128
- publicWsUrl: z.string().url().nullable().optional(),
129
311
  transportPolicy: RealtimeTransportPolicySchema.optional(),
130
- networkMode: LiveKitNetworkModeSchema.optional(),
131
- nodeIp: z.string().nullable().optional(),
312
+ rtcDataAudioPublicHost: RtcDataAudioPublicHostSchema.optional(),
313
+ rtcDataAudioPublicUdpPort: RtcDataAudioPublicUdpPortSchema.optional(),
132
314
  });
133
315
 
134
316
  export type RealtimeSettings = z.infer<typeof RealtimeSettingsSchema>;
135
317
 
136
- export const RealtimeCredentialSourceSchema = z.enum(['managed-file', 'environment-override', 'missing']);
137
- export type RealtimeCredentialSource = z.infer<typeof RealtimeCredentialSourceSchema>;
138
-
139
- export const RealtimeCredentialStatusSchema = z.object({
140
- initialized: z.boolean(),
141
- source: RealtimeCredentialSourceSchema,
142
- filePath: z.string().nullable(),
143
- apiKeyPreview: z.string().nullable(),
144
- createdAt: z.string().nullable(),
145
- rotatedAt: z.string().nullable(),
318
+ export const RealtimeRtcDataAudioRuntimeSchema = z.object({
319
+ localUdpPort: z.number().int().min(1).max(65535),
320
+ publicCandidateEnabled: z.boolean(),
321
+ publicEndpoint: z.object({
322
+ host: z.string(),
323
+ port: z.number().int().min(1).max(65535),
324
+ }).nullable(),
146
325
  });
147
- export type RealtimeCredentialStatus = z.infer<typeof RealtimeCredentialStatusSchema>;
326
+ export type RealtimeRtcDataAudioRuntime = z.infer<typeof RealtimeRtcDataAudioRuntimeSchema>;
148
327
 
149
328
  export const RealtimeSettingsRuntimeSchema = z.object({
150
- liveKitEnabled: z.boolean(),
151
329
  connectivityHints: RealtimeConnectivityHintsSchema,
152
330
  radioReceiveTransport: RealtimeTransportKindSchema,
153
- radioBridgeHealthy: z.boolean(),
154
- radioBridgeIssueCode: RealtimeConnectivityErrorCodeSchema.nullable(),
155
- credentialStatus: RealtimeCredentialStatusSchema,
331
+ rtcDataAudio: RealtimeRtcDataAudioRuntimeSchema,
156
332
  });
157
333
 
158
334
  export type RealtimeSettingsRuntime = z.infer<typeof RealtimeSettingsRuntimeSchema>;
@@ -234,6 +410,10 @@ export const RealtimeVoiceTxServerIngressStatsSchema = z.object({
234
410
  queueDepthFrames: z.number().int().nonnegative(),
235
411
  queuedAudioMs: z.number().nonnegative(),
236
412
  droppedFrames: z.number().int().nonnegative(),
413
+ staleDroppedFrames: z.number().int().nonnegative(),
414
+ underrunCount: z.number().int().nonnegative(),
415
+ plcFrames: z.number().int().nonnegative(),
416
+ jitterTargetMs: z.number().nonnegative(),
237
417
  });
238
418
 
239
419
  export type RealtimeVoiceTxServerIngressStats = z.infer<typeof RealtimeVoiceTxServerIngressStatsSchema>;
@@ -242,8 +422,10 @@ export const RealtimeVoiceTxServerOutputStatsSchema = z.object({
242
422
  resampleMs: RealtimeVoiceTxMetricWindowSchema,
243
423
  queueWaitMs: RealtimeVoiceTxMetricWindowSchema,
244
424
  writeMs: RealtimeVoiceTxMetricWindowSchema,
425
+ serverPipelineMs: RealtimeVoiceTxMetricWindowSchema,
245
426
  endToEndMs: RealtimeVoiceTxMetricWindowSchema,
246
427
  outputBufferedMs: RealtimeVoiceTxMetricWindowSchema,
428
+ outputWriteIntervalMs: RealtimeVoiceTxMetricWindowSchema,
247
429
  outputSampleRate: z.number().nullable(),
248
430
  outputBufferSize: z.number().nullable(),
249
431
  writeFailures: z.number().int().nonnegative(),
@@ -18,8 +18,8 @@ export enum ServerMessageKey {
18
18
  /** 实时语音连接异常 */
19
19
  REALTIME_CONNECTIVITY_WARNING = 'realtimeConnectivityWarning',
20
20
 
21
- /** 服务端实时语音桥接异常 */
22
- REALTIME_BRIDGE_DOWN = 'realtimeBridgeDown',
21
+ /** 服务端实时语音服务异常 */
22
+ REALTIME_SERVICE_DOWN = 'realtimeServiceDown',
23
23
 
24
24
  /** 实时语音链路无音频数据 */
25
25
  REALTIME_NO_AUDIO = 'realtimeNoAudio',
@@ -148,7 +148,9 @@ export const SlotPackSchema = z.object({
148
148
  /** 去重后的帧数 */
149
149
  totalFramesAfterDedup: z.number().default(0),
150
150
  /** 最后更新时间戳 */
151
- lastUpdated: z.number().default(() => Date.now())
151
+ lastUpdated: z.number().default(() => Date.now()),
152
+ /** SlotPack 单调更新序号,用于客户端丢弃乱序旧包 */
153
+ updateSeq: z.number().int().nonnegative().optional()
152
154
  }).default({}),
153
155
  /** 解码历史(用于调试) */
154
156
  decodeHistory: z.array(z.object({
@@ -187,6 +187,7 @@ export enum WSMessageType {
187
187
  PLUGIN_USER_ACTION = 'pluginUserAction',
188
188
  PLUGIN_PAGE_PUSH = 'pluginPagePush',
189
189
  PLUGIN_PANEL_META = 'pluginPanelMeta',
190
+ PLUGIN_PANEL_CONTRIBUTIONS_CHANGED = 'pluginPanelContributionsChanged',
190
191
 
191
192
  // ===== OpenWebRX SDR =====
192
193
  OPENWEBRX_LISTEN_STATUS = 'openwebrxListenStatus',
@@ -216,7 +217,7 @@ export const SystemStatusSchema = z.object({
216
217
  /** 当前电台调制模式(语音模式下使用,如 USB/LSB/FM/AM) */
217
218
  currentRadioMode: z.string().optional(),
218
219
  /** 引擎状态机当前状态 */
219
- engineState: z.enum(['idle', 'waking', 'starting', 'running', 'stopping']).optional(),
220
+ engineState: z.enum(['idle', 'starting', 'running', 'stopping']).optional(),
220
221
  /** 引擎状态机上下文的精简投影 */
221
222
  engineContext: z.object({
222
223
  error: z.string().optional(),
@@ -1490,6 +1491,7 @@ export interface DigitalRadioEngineEvents {
1490
1491
  data?: unknown;
1491
1492
  }) => void;
1492
1493
  pluginPanelMeta: (data: import('./plugin.schema.js').PluginPanelMetaPayload) => void;
1494
+ pluginPanelContributionsChanged: (data: import('./plugin.schema.js').PluginUIPanelContributionGroup) => void;
1493
1495
 
1494
1496
  // 内部子系统间通信事件(不通过 WebSocket 广播,用于 engine emitter 内部编排)
1495
1497
  encodeStart: (slotInfo: z.infer<typeof SlotInfoSchema>) => void;