@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.
- package/dist/schema/__tests__/radio-power-support.test.d.ts +2 -0
- package/dist/schema/__tests__/radio-power-support.test.d.ts.map +1 -0
- package/dist/schema/__tests__/radio-power-support.test.js +19 -0
- package/dist/schema/__tests__/radio-power-support.test.js.map +1 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.d.ts +2 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.d.ts.map +1 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.js +122 -0
- package/dist/schema/__tests__/realtime-settings.schema.test.js.map +1 -0
- package/dist/schema/cpu-profile.schema.d.ts +4 -4
- package/dist/schema/desktop-https.schema.d.ts +9 -0
- package/dist/schema/desktop-https.schema.d.ts.map +1 -1
- package/dist/schema/desktop-https.schema.js +3 -0
- package/dist/schema/desktop-https.schema.js.map +1 -1
- package/dist/schema/openwebrx.schema.d.ts +1 -1
- package/dist/schema/openwebrx.schema.d.ts.map +1 -1
- package/dist/schema/openwebrx.schema.js +1 -1
- package/dist/schema/openwebrx.schema.js.map +1 -1
- package/dist/schema/operator.schema.d.ts +32 -32
- package/dist/schema/plugin.schema.d.ts +702 -168
- package/dist/schema/plugin.schema.d.ts.map +1 -1
- package/dist/schema/plugin.schema.js +38 -3
- package/dist/schema/plugin.schema.js.map +1 -1
- package/dist/schema/radio-capability.schema.d.ts +42 -2
- package/dist/schema/radio-capability.schema.d.ts.map +1 -1
- package/dist/schema/radio-capability.schema.js +18 -1
- package/dist/schema/radio-capability.schema.js.map +1 -1
- package/dist/schema/radio-power-support.d.ts +8 -0
- package/dist/schema/radio-power-support.d.ts.map +1 -1
- package/dist/schema/radio-power-support.js +38 -26
- package/dist/schema/radio-power-support.js.map +1 -1
- package/dist/schema/radio-power.schema.d.ts +10 -6
- package/dist/schema/radio-power.schema.d.ts.map +1 -1
- package/dist/schema/radio-power.schema.js +8 -6
- package/dist/schema/radio-power.schema.js.map +1 -1
- package/dist/schema/realtime.schema.d.ts +797 -417
- package/dist/schema/realtime.schema.d.ts.map +1 -1
- package/dist/schema/realtime.schema.js +205 -51
- package/dist/schema/realtime.schema.js.map +1 -1
- package/dist/schema/server-message-keys.d.ts +2 -2
- package/dist/schema/server-message-keys.d.ts.map +1 -1
- package/dist/schema/server-message-keys.js +2 -2
- package/dist/schema/server-message-keys.js.map +1 -1
- package/dist/schema/slot-info.schema.d.ts +6 -0
- package/dist/schema/slot-info.schema.d.ts.map +1 -1
- package/dist/schema/slot-info.schema.js +3 -1
- package/dist/schema/slot-info.schema.js.map +1 -1
- package/dist/schema/websocket.schema.d.ts +1436 -640
- package/dist/schema/websocket.schema.d.ts.map +1 -1
- package/dist/schema/websocket.schema.js +2 -1
- package/dist/schema/websocket.schema.js.map +1 -1
- package/package.json +1 -1
- package/src/schema/__tests__/radio-power-support.test.ts +26 -0
- package/src/schema/__tests__/realtime-settings.schema.test.ts +142 -0
- package/src/schema/desktop-https.schema.ts +3 -1
- package/src/schema/openwebrx.schema.ts +1 -1
- package/src/schema/plugin.schema.ts +44 -3
- package/src/schema/radio-capability.schema.ts +25 -1
- package/src/schema/radio-power-support.ts +49 -26
- package/src/schema/radio-power.schema.ts +8 -6
- package/src/schema/realtime.schema.ts +247 -65
- package/src/schema/server-message-keys.ts +2 -2
- package/src/schema/slot-info.schema.ts +3 -1
- 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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
'
|
|
116
|
-
'
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
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;
|