@byokit/realtime 0.3.0 → 0.3.1
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 +6 -0
- package/README.md +4 -2
- package/dist/webrtc.js +42 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.1 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
5
11
|
## 0.3.0 (2026-10-01)
|
|
6
12
|
|
|
7
13
|
|
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;
|
package/dist/webrtc.js
CHANGED
|
@@ -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)
|