@agent-native/core 0.114.6 → 0.114.8

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 (155) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/core/src/server/agent-chat-plugin.ts +16 -9
  14. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +19 -7
  15. package/corpus/templates/analytics/app/hooks/use-chart-tooltip-portal.ts +60 -0
  16. package/corpus/templates/analytics/app/routes/chart.tsx +1 -1
  17. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-requests-no-longer-stall-without-progress.md +6 -0
  18. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
  19. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  20. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  21. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  22. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  23. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  24. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  25. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  26. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  27. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  28. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  29. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  30. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  31. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  32. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  33. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  34. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  35. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  36. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  37. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  38. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  50. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  53. package/corpus/templates/clips/desktop/package.json +1 -0
  54. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  55. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  56. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  57. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  59. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  60. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  61. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  62. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  63. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  64. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  65. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  66. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  67. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  68. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  69. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  70. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  71. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  72. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  73. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  74. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  76. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  77. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  78. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  79. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  80. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  82. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  83. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  89. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  92. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  93. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  94. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  96. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  97. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  98. package/corpus/toolkit/CHANGELOG.md +6 -0
  99. package/corpus/toolkit/package.json +1 -1
  100. package/dist/cli/mcp-config-writers.d.ts +2 -2
  101. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  102. package/dist/cli/mcp-config-writers.js +6 -6
  103. package/dist/cli/mcp-config-writers.js.map +1 -1
  104. package/dist/cli/mcp.d.ts +14 -0
  105. package/dist/cli/mcp.d.ts.map +1 -1
  106. package/dist/cli/mcp.js +79 -0
  107. package/dist/cli/mcp.js.map +1 -1
  108. package/dist/cli/skills-content/help.d.ts +1 -1
  109. package/dist/cli/skills-content/help.d.ts.map +1 -1
  110. package/dist/cli/skills-content/help.js +4 -3
  111. package/dist/cli/skills-content/help.js.map +1 -1
  112. package/dist/cli/skills-content/index.d.ts +1 -0
  113. package/dist/cli/skills-content/index.d.ts.map +1 -1
  114. package/dist/cli/skills-content/index.js +1 -0
  115. package/dist/cli/skills-content/index.js.map +1 -1
  116. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  117. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  118. package/dist/cli/skills-content/rewind-skill.js +53 -0
  119. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  120. package/dist/cli/skills.d.ts +6 -0
  121. package/dist/cli/skills.d.ts.map +1 -1
  122. package/dist/cli/skills.js +90 -14
  123. package/dist/cli/skills.js.map +1 -1
  124. package/dist/collab/struct-routes.d.ts +1 -1
  125. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  126. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  127. package/dist/mcp/screen-memory-stdio.js +1014 -91
  128. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  129. package/dist/mcp-client/index.d.ts +1 -1
  130. package/dist/mcp-client/index.d.ts.map +1 -1
  131. package/dist/mcp-client/index.js +1 -1
  132. package/dist/mcp-client/index.js.map +1 -1
  133. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  134. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  135. package/dist/mcp-client/screen-memory-local.js +590 -12
  136. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  137. package/dist/observability/routes.d.ts +1 -1
  138. package/dist/progress/routes.d.ts +1 -1
  139. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  140. package/dist/resources/handlers.d.ts +1 -1
  141. package/dist/server/agent-chat-plugin.d.ts +5 -0
  142. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  143. package/dist/server/agent-chat-plugin.js +10 -9
  144. package/dist/server/agent-chat-plugin.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/cli/mcp-config-writers.ts +6 -4
  147. package/src/cli/mcp.ts +124 -0
  148. package/src/cli/skills-content/help.ts +4 -3
  149. package/src/cli/skills-content/index.ts +1 -0
  150. package/src/cli/skills-content/rewind-skill.ts +52 -0
  151. package/src/cli/skills.ts +115 -13
  152. package/src/mcp/screen-memory-stdio.ts +1360 -104
  153. package/src/mcp-client/index.ts +9 -0
  154. package/src/mcp-client/screen-memory-local.ts +847 -13
  155. package/src/server/agent-chat-plugin.ts +16 -9
@@ -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
+ }