@agent-native/core 0.114.6 → 0.114.7
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
//! In-process fan-out for PCM already produced by the Rewind ScreenCaptureKit stream.
|
|
2
|
+
//!
|
|
3
|
+
//! The bus deliberately owns no hardware. A single producer advertises the
|
|
4
|
+
//! sources it has opened and publishes mono f32 buffers; consumers either
|
|
5
|
+
//! subscribe to that producer or, when none exists, may open their legacy
|
|
6
|
+
//! physical capture path. An existing producer that lacks a requested source
|
|
7
|
+
//! is a hard error, never permission to start a second recorder.
|
|
8
|
+
|
|
9
|
+
use std::collections::BTreeMap;
|
|
10
|
+
use std::fmt;
|
|
11
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
12
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
13
|
+
|
|
14
|
+
pub(crate) type AudioCallback = Arc<dyn Fn(&[f32], f64) + Send + Sync>;
|
|
15
|
+
|
|
16
|
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
17
|
+
pub(crate) struct AudioSources {
|
|
18
|
+
pub microphone: bool,
|
|
19
|
+
pub system: bool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl AudioSources {
|
|
23
|
+
pub(crate) const fn new(microphone: bool, system: bool) -> Self {
|
|
24
|
+
Self { microphone, system }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn contains(self, requested: Self) -> bool {
|
|
28
|
+
(!requested.microphone || self.microphone) && (!requested.system || self.system)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn missing_names(self, requested: Self) -> Vec<&'static str> {
|
|
32
|
+
let mut missing = Vec::new();
|
|
33
|
+
if requested.microphone && !self.microphone {
|
|
34
|
+
missing.push("microphone");
|
|
35
|
+
}
|
|
36
|
+
if requested.system && !self.system {
|
|
37
|
+
missing.push("system");
|
|
38
|
+
}
|
|
39
|
+
missing
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
44
|
+
pub(crate) enum CapturePlan {
|
|
45
|
+
OpenPhysicalCapture,
|
|
46
|
+
SubscribeToSharedProducer,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
50
|
+
pub(crate) enum SubscribeError {
|
|
51
|
+
ProducerLacksSources {
|
|
52
|
+
available: AudioSources,
|
|
53
|
+
requested: AudioSources,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl fmt::Display for SubscribeError {
|
|
58
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
59
|
+
match self {
|
|
60
|
+
Self::ProducerLacksSources {
|
|
61
|
+
available,
|
|
62
|
+
requested,
|
|
63
|
+
} => write!(
|
|
64
|
+
formatter,
|
|
65
|
+
"shared-audio-producer-lacks-sources:{}",
|
|
66
|
+
available.missing_names(*requested).join(",")
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl std::error::Error for SubscribeError {}
|
|
73
|
+
|
|
74
|
+
pub(crate) fn capture_plan(
|
|
75
|
+
available: Option<AudioSources>,
|
|
76
|
+
requested: AudioSources,
|
|
77
|
+
) -> Result<CapturePlan, SubscribeError> {
|
|
78
|
+
match available {
|
|
79
|
+
None => Ok(CapturePlan::OpenPhysicalCapture),
|
|
80
|
+
Some(available) if available.contains(requested) => {
|
|
81
|
+
Ok(CapturePlan::SubscribeToSharedProducer)
|
|
82
|
+
}
|
|
83
|
+
Some(available) => Err(SubscribeError::ProducerLacksSources {
|
|
84
|
+
available,
|
|
85
|
+
requested,
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct Subscriber {
|
|
91
|
+
active: Arc<AtomicBool>,
|
|
92
|
+
microphone: Option<AudioCallback>,
|
|
93
|
+
system: Option<AudioCallback>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct ProducerState {
|
|
97
|
+
id: u64,
|
|
98
|
+
sources: AudioSources,
|
|
99
|
+
subscribers: BTreeMap<u64, Subscriber>,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[derive(Default)]
|
|
103
|
+
struct BusState {
|
|
104
|
+
next_id: u64,
|
|
105
|
+
producer: Option<ProducerState>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn bus() -> &'static Mutex<BusState> {
|
|
109
|
+
static BUS: OnceLock<Mutex<BusState>> = OnceLock::new();
|
|
110
|
+
BUS.get_or_init(|| Mutex::new(BusState::default()))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
struct ProducerRegistration {
|
|
114
|
+
id: u64,
|
|
115
|
+
active: AtomicBool,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
impl ProducerRegistration {
|
|
119
|
+
fn deactivate(&self) {
|
|
120
|
+
if !self.active.swap(false, Ordering::SeqCst) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if let Ok(mut state) = bus().lock() {
|
|
124
|
+
if state
|
|
125
|
+
.producer
|
|
126
|
+
.as_ref()
|
|
127
|
+
.is_some_and(|producer| producer.id == self.id)
|
|
128
|
+
{
|
|
129
|
+
if let Some(producer) = state.producer.take() {
|
|
130
|
+
for subscriber in producer.subscribers.into_values() {
|
|
131
|
+
subscriber.active.store(false, Ordering::SeqCst);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl Drop for ProducerRegistration {
|
|
140
|
+
fn drop(&mut self) {
|
|
141
|
+
self.deactivate();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Cloneable producer handle. The registration remains live through stream
|
|
146
|
+
/// rebuilds until explicitly deactivated or the final clone is dropped.
|
|
147
|
+
#[derive(Clone)]
|
|
148
|
+
pub(crate) struct AudioProducer {
|
|
149
|
+
registration: Arc<ProducerRegistration>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
impl AudioProducer {
|
|
153
|
+
pub(crate) fn register(sources: AudioSources) -> Result<Self, String> {
|
|
154
|
+
let mut state = bus().lock().map_err(|error| error.to_string())?;
|
|
155
|
+
if state.producer.is_some() {
|
|
156
|
+
return Err("shared-audio-producer-already-active".into());
|
|
157
|
+
}
|
|
158
|
+
state.next_id = state.next_id.saturating_add(1);
|
|
159
|
+
let id = state.next_id;
|
|
160
|
+
state.producer = Some(ProducerState {
|
|
161
|
+
id,
|
|
162
|
+
sources,
|
|
163
|
+
subscribers: BTreeMap::new(),
|
|
164
|
+
});
|
|
165
|
+
Ok(Self {
|
|
166
|
+
registration: Arc::new(ProducerRegistration {
|
|
167
|
+
id,
|
|
168
|
+
active: AtomicBool::new(true),
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub(crate) fn deactivate(&self) {
|
|
174
|
+
self.registration.deactivate();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub(crate) fn publish_microphone(&self, samples: &[f32], sample_rate: f64) {
|
|
178
|
+
self.publish(samples, sample_rate, true);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pub(crate) fn publish_system(&self, samples: &[f32], sample_rate: f64) {
|
|
182
|
+
self.publish(samples, sample_rate, false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn publish(&self, samples: &[f32], sample_rate: f64, microphone: bool) {
|
|
186
|
+
if samples.is_empty() || !self.registration.active.load(Ordering::SeqCst) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let callbacks: Vec<(Arc<AtomicBool>, AudioCallback)> = bus()
|
|
190
|
+
.lock()
|
|
191
|
+
.ok()
|
|
192
|
+
.and_then(|state| {
|
|
193
|
+
state.producer.as_ref().and_then(|producer| {
|
|
194
|
+
(producer.id == self.registration.id).then(|| {
|
|
195
|
+
producer
|
|
196
|
+
.subscribers
|
|
197
|
+
.values()
|
|
198
|
+
.filter_map(|subscriber| {
|
|
199
|
+
let callback = if microphone {
|
|
200
|
+
subscriber.microphone.as_ref()
|
|
201
|
+
} else {
|
|
202
|
+
subscriber.system.as_ref()
|
|
203
|
+
}?;
|
|
204
|
+
Some((subscriber.active.clone(), callback.clone()))
|
|
205
|
+
})
|
|
206
|
+
.collect()
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
.unwrap_or_default();
|
|
211
|
+
// Never hold the bus lock while invoking client code. A callback may
|
|
212
|
+
// stop its session and drop its own subscription.
|
|
213
|
+
for (active, callback) in callbacks {
|
|
214
|
+
if active.load(Ordering::SeqCst) {
|
|
215
|
+
callback(samples, sample_rate);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pub(crate) enum SubscriptionAttempt {
|
|
222
|
+
NoProducer,
|
|
223
|
+
Subscribed(AudioSubscription),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pub(crate) struct AudioSubscription {
|
|
227
|
+
producer_id: u64,
|
|
228
|
+
subscriber_id: u64,
|
|
229
|
+
active: Arc<AtomicBool>,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
impl Drop for AudioSubscription {
|
|
233
|
+
fn drop(&mut self) {
|
|
234
|
+
self.active.store(false, Ordering::SeqCst);
|
|
235
|
+
if let Ok(mut state) = bus().lock() {
|
|
236
|
+
if let Some(producer) = state.producer.as_mut() {
|
|
237
|
+
if producer.id == self.producer_id {
|
|
238
|
+
producer.subscribers.remove(&self.subscriber_id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
pub(crate) fn try_subscribe(
|
|
246
|
+
requested: AudioSources,
|
|
247
|
+
microphone: Option<AudioCallback>,
|
|
248
|
+
system: Option<AudioCallback>,
|
|
249
|
+
) -> Result<SubscriptionAttempt, SubscribeError> {
|
|
250
|
+
let mut state = bus().lock().expect("capture audio bus lock poisoned");
|
|
251
|
+
let available = state.producer.as_ref().map(|producer| producer.sources);
|
|
252
|
+
match capture_plan(available, requested)? {
|
|
253
|
+
CapturePlan::OpenPhysicalCapture => Ok(SubscriptionAttempt::NoProducer),
|
|
254
|
+
CapturePlan::SubscribeToSharedProducer => {
|
|
255
|
+
state.next_id = state.next_id.saturating_add(1);
|
|
256
|
+
let subscriber_id = state.next_id;
|
|
257
|
+
let producer = state
|
|
258
|
+
.producer
|
|
259
|
+
.as_mut()
|
|
260
|
+
.expect("capture plan observed an active producer");
|
|
261
|
+
let producer_id = producer.id;
|
|
262
|
+
let active = Arc::new(AtomicBool::new(true));
|
|
263
|
+
producer.subscribers.insert(
|
|
264
|
+
subscriber_id,
|
|
265
|
+
Subscriber {
|
|
266
|
+
active: active.clone(),
|
|
267
|
+
microphone,
|
|
268
|
+
system,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
Ok(SubscriptionAttempt::Subscribed(AudioSubscription {
|
|
272
|
+
producer_id,
|
|
273
|
+
subscriber_id,
|
|
274
|
+
active,
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[cfg(test)]
|
|
281
|
+
mod tests {
|
|
282
|
+
use super::*;
|
|
283
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
284
|
+
use std::sync::MutexGuard;
|
|
285
|
+
|
|
286
|
+
fn test_guard() -> MutexGuard<'static, ()> {
|
|
287
|
+
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
288
|
+
TEST_LOCK
|
|
289
|
+
.get_or_init(|| Mutex::new(()))
|
|
290
|
+
.lock()
|
|
291
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn reset() {
|
|
295
|
+
*bus().lock().unwrap() = BusState::default();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn routes_sources_to_independent_subscribers() {
|
|
300
|
+
let _guard = test_guard();
|
|
301
|
+
reset();
|
|
302
|
+
let producer = AudioProducer::register(AudioSources::new(true, true)).unwrap();
|
|
303
|
+
let mic_frames = Arc::new(AtomicUsize::new(0));
|
|
304
|
+
let sys_frames = Arc::new(AtomicUsize::new(0));
|
|
305
|
+
let mic_seen = mic_frames.clone();
|
|
306
|
+
let sys_seen = sys_frames.clone();
|
|
307
|
+
let _subscription = match try_subscribe(
|
|
308
|
+
AudioSources::new(true, true),
|
|
309
|
+
Some(Arc::new(move |samples, rate| {
|
|
310
|
+
assert_eq!(rate, 44_100.0);
|
|
311
|
+
mic_seen.fetch_add(samples.len(), Ordering::SeqCst);
|
|
312
|
+
})),
|
|
313
|
+
Some(Arc::new(move |samples, rate| {
|
|
314
|
+
assert_eq!(rate, 48_000.0);
|
|
315
|
+
sys_seen.fetch_add(samples.len(), Ordering::SeqCst);
|
|
316
|
+
})),
|
|
317
|
+
)
|
|
318
|
+
.unwrap()
|
|
319
|
+
{
|
|
320
|
+
SubscriptionAttempt::Subscribed(subscription) => subscription,
|
|
321
|
+
SubscriptionAttempt::NoProducer => panic!("producer disappeared"),
|
|
322
|
+
};
|
|
323
|
+
producer.publish_microphone(&[0.1, 0.2], 44_100.0);
|
|
324
|
+
producer.publish_system(&[0.3, 0.4, 0.5], 48_000.0);
|
|
325
|
+
assert_eq!(mic_frames.load(Ordering::SeqCst), 2);
|
|
326
|
+
assert_eq!(sys_frames.load(Ordering::SeqCst), 3);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn clone_refcount_and_subscription_drop_keep_teardown_safe() {
|
|
331
|
+
let _guard = test_guard();
|
|
332
|
+
reset();
|
|
333
|
+
let producer = AudioProducer::register(AudioSources::new(true, false)).unwrap();
|
|
334
|
+
let producer_clone = producer.clone();
|
|
335
|
+
drop(producer);
|
|
336
|
+
assert_eq!(
|
|
337
|
+
capture_plan(
|
|
338
|
+
bus().lock().unwrap().producer.as_ref().map(|p| p.sources),
|
|
339
|
+
AudioSources::new(true, false)
|
|
340
|
+
),
|
|
341
|
+
Ok(CapturePlan::SubscribeToSharedProducer)
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
let calls = Arc::new(AtomicUsize::new(0));
|
|
345
|
+
let seen = calls.clone();
|
|
346
|
+
let subscription = match try_subscribe(
|
|
347
|
+
AudioSources::new(true, false),
|
|
348
|
+
Some(Arc::new(move |_, _| {
|
|
349
|
+
seen.fetch_add(1, Ordering::SeqCst);
|
|
350
|
+
})),
|
|
351
|
+
None,
|
|
352
|
+
)
|
|
353
|
+
.unwrap()
|
|
354
|
+
{
|
|
355
|
+
SubscriptionAttempt::Subscribed(subscription) => subscription,
|
|
356
|
+
SubscriptionAttempt::NoProducer => panic!("producer disappeared"),
|
|
357
|
+
};
|
|
358
|
+
producer_clone.publish_microphone(&[0.1], 48_000.0);
|
|
359
|
+
drop(subscription);
|
|
360
|
+
producer_clone.publish_microphone(&[0.2], 48_000.0);
|
|
361
|
+
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
|
362
|
+
|
|
363
|
+
drop(producer_clone);
|
|
364
|
+
assert!(bus().lock().unwrap().producer.is_none());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn existing_producer_without_sources_fails_closed() {
|
|
369
|
+
let _guard = test_guard();
|
|
370
|
+
reset();
|
|
371
|
+
assert_eq!(
|
|
372
|
+
capture_plan(
|
|
373
|
+
Some(AudioSources::new(true, false)),
|
|
374
|
+
AudioSources::new(true, true)
|
|
375
|
+
),
|
|
376
|
+
Err(SubscribeError::ProducerLacksSources {
|
|
377
|
+
available: AudioSources::new(true, false),
|
|
378
|
+
requested: AudioSources::new(true, true),
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|