@byokit/realtime 0.3.0 → 0.3.2
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/CHANGELOG.md +10 -0
- package/README.md +5 -3
- package/dist/chatgpt.js +2 -2
- package/dist/child.js +2 -2
- package/dist/client.js +1 -1
- package/dist/index.js +7 -7
- package/dist/node.js +6 -6
- package/dist/providers/gemini.js +2 -2
- package/dist/providers/openai.js +2 -2
- package/dist/providers/xai.js +2 -2
- package/dist/rn-webrtc.js +1 -1
- package/dist/tools.js +1 -1
- package/dist/webrtc.js +43 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.2 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- Dependency update: ws 8.22.0.
|
|
8
|
+
|
|
9
|
+
## 0.3.1 (2026-10-01)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- SECURITY: Lazy WebRTC capture no longer opens the Android microphone or sends untapped speech before `attachMic()`; `releaseMic()` closes recording while keeping the call connected. Apps using lazy capture should update.
|
|
14
|
+
|
|
5
15
|
## 0.3.0 (2026-10-01)
|
|
6
16
|
|
|
7
17
|
|
package/README.md
CHANGED
|
@@ -194,8 +194,10 @@ function endCall() { client.stop(); }
|
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
`attachMic()` awaits the app's microphone permission/service lease, captures
|
|
197
|
-
an audio track and uses `sender.replaceTrack(track)
|
|
198
|
-
|
|
197
|
+
an audio track and uses `sender.replaceTrack(track)` before enabling the sender's
|
|
198
|
+
encoding. Lazy peers negotiate `sendrecv` with an inactive encoding: Android's
|
|
199
|
+
audio device cannot start recording just because an answer is applied.
|
|
200
|
+
`releaseMic()` disables that encoding before `replaceTrack(null)`, stops capture tracks and releases the lease so the app
|
|
199
201
|
can stop its microphone service and clear the OS indicator. Neither operation
|
|
200
202
|
renegotiates. Repeated calls are safe; release or stop during pending capture
|
|
201
203
|
cleans up when that capture resolves. Reconnecting starts another lazy peer;
|
|
@@ -256,4 +258,4 @@ Credentials, endpoint restrictions and redirect rejection are unchanged.
|
|
|
256
258
|
The identity option allows a consumer to compare signaling latency; offline
|
|
257
259
|
tests prove startup ordering, not a reduction in the remote provider's POST time.
|
|
258
260
|
|
|
259
|
-
For an explicitly live microphone and subscription-provider proof, run [the bounded voice demo](../../examples/realtime-voice/README.md). It signs in through accounts,
|
|
261
|
+
For an explicitly live microphone and subscription-provider proof, run [the bounded voice demo](../../examples/realtime-voice/README.md). It signs in through accounts, keeping the sign-in at this computer's machine store, and records connection facts and turn counts. Offline tests do not establish a live pass.
|
package/dist/chatgpt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cleanProse, cleanProseBytes } from
|
|
2
|
-
import { MAX_REALTIME_SDP_BYTES } from
|
|
1
|
+
import { cleanProse, cleanProseBytes } from './prose.js';
|
|
2
|
+
import { MAX_REALTIME_SDP_BYTES } from './frames.js';
|
|
3
3
|
export const CHATGPT_SIGNALING_URL = 'https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas';
|
|
4
4
|
function emit(frame) { process.stdout.write(`${JSON.stringify(frame)}\n`); }
|
|
5
5
|
export async function boundedBody(response, max = MAX_REALTIME_SDP_BYTES) {
|
package/dist/child.js
CHANGED
|
@@ -8,8 +8,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { createInterface } from 'node:readline';
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import { parseRealtimeClientFrame } from
|
|
12
|
-
import { createChatgptAdapter } from
|
|
11
|
+
import { parseRealtimeClientFrame } from './frames.js';
|
|
12
|
+
import { createChatgptAdapter } from './chatgpt.js';
|
|
13
13
|
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
14
14
|
let adapter;
|
|
15
15
|
const pending = new Map();
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseRealtimeClientFrame, parseRealtimeHostFrame, realtimePcm16ByteLength } from
|
|
1
|
+
import { parseRealtimeClientFrame, parseRealtimeHostFrame, realtimePcm16ByteLength } from './frames.js';
|
|
2
2
|
/** Provider-blind client with bounded reconnects and exclusive media ownership. */
|
|
3
3
|
export function realtimeClient(options) {
|
|
4
4
|
const audio = options.audio;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export { providers } from
|
|
1
|
+
export * from './frames.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './words.js';
|
|
4
|
+
export * from './prose.js';
|
|
5
|
+
export * from './client.js';
|
|
6
|
+
export * from './auth.js';
|
|
7
|
+
export { providers } from './catalogue.js';
|
package/dist/node.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { parseRealtimeClientFrame, parseRealtimeHostFrame } from
|
|
5
|
-
import { RealtimeError } from
|
|
6
|
-
import { cleanProseBytes } from
|
|
7
|
-
import { realtimeAuthCheck } from
|
|
8
|
-
export { toolBridge, appBridge, delegationHandler } from
|
|
9
|
-
export { realtimeAuthCheck } from
|
|
4
|
+
import { parseRealtimeClientFrame, parseRealtimeHostFrame } from './frames.js';
|
|
5
|
+
import { RealtimeError } from './types.js';
|
|
6
|
+
import { cleanProseBytes } from './prose.js';
|
|
7
|
+
import { realtimeAuthCheck } from './auth.js';
|
|
8
|
+
export { toolBridge, appBridge, delegationHandler } from './tools.js';
|
|
9
|
+
export { realtimeAuthCheck } from './auth.js';
|
|
10
10
|
/** Provider sockets and SDP signaling run in a kit-owned child, never in the app process. */
|
|
11
11
|
export function realtimeEngine(options) {
|
|
12
12
|
if (!['chatgpt', 'openai', 'gemini', 'xai'].includes(options.engine))
|
package/dist/providers/gemini.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { responseFence } from
|
|
3
|
-
import { cleanProse, providerRefusal as refusal } from
|
|
2
|
+
import { responseFence } from '../policy.js';
|
|
3
|
+
import { cleanProse, providerRefusal as refusal } from '../prose.js';
|
|
4
4
|
// Lifted wire behavior; this module is loaded only by the kit child process.
|
|
5
5
|
export function createAdapter(options) {
|
|
6
6
|
const MODEL = options.model ?? 'gemini-3.8-live';
|
package/dist/providers/openai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { responseFence } from
|
|
3
|
-
import { cleanProse, providerRefusal as refusal } from
|
|
2
|
+
import { responseFence } from '../policy.js';
|
|
3
|
+
import { cleanProse, providerRefusal as refusal } from '../prose.js';
|
|
4
4
|
// Lifted wire behavior; this module is loaded only by the kit child process.
|
|
5
5
|
export function createAdapter(options) {
|
|
6
6
|
const MODEL = options.model ?? 'gpt-realtime-2.1';
|
package/dist/providers/xai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { responseFence } from
|
|
3
|
-
import { cleanProse, providerRefusal as refusal } from
|
|
2
|
+
import { responseFence } from '../policy.js';
|
|
3
|
+
import { cleanProse, providerRefusal as refusal } from '../prose.js';
|
|
4
4
|
// Lifted wire behavior; this module is loaded only by the kit child process.
|
|
5
5
|
export function createAdapter(options) {
|
|
6
6
|
const MODEL = options.model ?? 'grok-voice-think-fast-2.0';
|
package/dist/rn-webrtc.js
CHANGED
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { cleanProseBytes } from
|
|
2
|
+
import { cleanProseBytes } from './prose.js';
|
|
3
3
|
/** Bounded, deduplicated tool execution. Product authorization stays in handlers. */
|
|
4
4
|
export function toolBridge(options) {
|
|
5
5
|
const lifetime = new AbortController();
|
package/dist/webrtc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_REALTIME_SDP_BYTES, MAX_REALTIME_WEBRTC_DATA_BYTES } from
|
|
1
|
+
import { MAX_REALTIME_SDP_BYTES, MAX_REALTIME_WEBRTC_DATA_BYTES } from './frames.js';
|
|
2
2
|
function sdp(value) {
|
|
3
3
|
if (typeof value !== 'string' || !value.startsWith('v=0') || value.includes('\0') || new TextEncoder().encode(value).length > MAX_REALTIME_SDP_BYTES)
|
|
4
4
|
throw new Error('Invalid voice connection answer.');
|
|
@@ -47,6 +47,19 @@ export async function webRtcPeer(options) {
|
|
|
47
47
|
options.onError(error);
|
|
48
48
|
stop();
|
|
49
49
|
} };
|
|
50
|
+
const setSending = async (active) => {
|
|
51
|
+
if (options.capture !== 'lazy')
|
|
52
|
+
return;
|
|
53
|
+
const parameters = sender.getParameters();
|
|
54
|
+
if (!parameters.encodings.length)
|
|
55
|
+
throw new Error('Voice capture control is unavailable.');
|
|
56
|
+
parameters.encodings.forEach(encoding => { encoding.active = active; });
|
|
57
|
+
await sender.setParameters(parameters);
|
|
58
|
+
// The native bridge can resolve even when the engine rejects setParameters.
|
|
59
|
+
const applied = sender.getParameters().encodings;
|
|
60
|
+
if (!applied.length || applied.some(encoding => encoding.active !== active))
|
|
61
|
+
throw new Error('Voice capture control failed.');
|
|
62
|
+
};
|
|
50
63
|
const captureMic = async (generation) => {
|
|
51
64
|
await options.audio.microphone.acquire();
|
|
52
65
|
acquired = true;
|
|
@@ -79,14 +92,28 @@ export async function webRtcPeer(options) {
|
|
|
79
92
|
const captured = await captureMic(generation);
|
|
80
93
|
if (captured) {
|
|
81
94
|
await sender.replaceTrack(captured.getAudioTracks()[0]);
|
|
95
|
+
if (!stopped && generation === micEpoch)
|
|
96
|
+
await setSending(true);
|
|
82
97
|
if (stopped || generation !== micEpoch) {
|
|
83
|
-
if (!stopped)
|
|
98
|
+
if (!stopped) {
|
|
99
|
+
await setSending(false);
|
|
84
100
|
await sender.replaceTrack(null);
|
|
101
|
+
}
|
|
85
102
|
clearMic();
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
catch (error) {
|
|
107
|
+
// Never leave a trackless native send stream recording after a failure.
|
|
108
|
+
try {
|
|
109
|
+
if (!stopped && options.capture === 'lazy') {
|
|
110
|
+
await setSending(false);
|
|
111
|
+
await sender.replaceTrack(null);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
stop();
|
|
116
|
+
}
|
|
90
117
|
clearMic();
|
|
91
118
|
throw error;
|
|
92
119
|
}
|
|
@@ -97,8 +124,15 @@ export async function webRtcPeer(options) {
|
|
|
97
124
|
stream?.getAudioTracks().forEach(track => { track.enabled = false; });
|
|
98
125
|
return serializeMic(async () => {
|
|
99
126
|
try {
|
|
100
|
-
if (!stopped && stream)
|
|
127
|
+
if (!stopped && stream) {
|
|
128
|
+
await setSending(false);
|
|
101
129
|
await sender.replaceTrack(null);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (options.capture === 'lazy')
|
|
134
|
+
stop();
|
|
135
|
+
throw error;
|
|
102
136
|
}
|
|
103
137
|
finally {
|
|
104
138
|
clearMic();
|
|
@@ -153,8 +187,12 @@ export async function webRtcPeer(options) {
|
|
|
153
187
|
options.onConnectionState('connecting'); };
|
|
154
188
|
current.ontrack = event => { const track = event.track; if (track.kind !== 'audio' || stopped)
|
|
155
189
|
return; options.onRemoteAudio(!track.muted); track.onmute = () => options.onRemoteAudio(false); track.onunmute = () => options.onRemoteAudio(true); track.onended = () => options.onRemoteAudio(false); };
|
|
156
|
-
if (options.capture === 'lazy')
|
|
157
|
-
|
|
190
|
+
if (options.capture === 'lazy') {
|
|
191
|
+
// Android starts the device module for an active send stream even without
|
|
192
|
+
// a track. Disable the encoding before either description can be applied.
|
|
193
|
+
sender = current.addTransceiver('audio', { direction: 'sendrecv', sendEncodings: [{ active: false }] }).sender;
|
|
194
|
+
await setSending(false);
|
|
195
|
+
}
|
|
158
196
|
else {
|
|
159
197
|
const input = stream.getAudioTracks()[0];
|
|
160
198
|
if (!input)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/realtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Realtime voice sessions with host-held credentials and injected audio.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"ws": "8.
|
|
46
|
+
"ws": "8.22.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react-native-webrtc": "124.0.8"
|