@agent-native/core 0.114.5 → 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.
Files changed (210) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -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/client/resources/ResourceTree.tsx +11 -5
  11. package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
  12. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  13. package/corpus/core/src/mcp-client/index.ts +9 -0
  14. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  15. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  16. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  17. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  18. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  19. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  20. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  21. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  22. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  23. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  24. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  25. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  26. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  27. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  28. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  29. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
  30. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  31. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  32. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  33. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  34. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  35. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  47. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  51. package/corpus/templates/clips/desktop/package.json +1 -0
  52. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  53. package/corpus/templates/clips/desktop/src/app.tsx +2478 -223
  54. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  56. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  57. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  58. package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  60. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  61. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  62. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  63. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  64. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
  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/templates/clips/shared/meeting-join-url.ts +0 -5
  99. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
  100. package/corpus/templates/content/AGENTS.md +4 -1
  101. package/corpus/templates/content/actions/_content-spaces.ts +6 -1
  102. package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
  103. package/corpus/templates/content/actions/_database-utils.ts +83 -15
  104. package/corpus/templates/content/actions/add-database-item.ts +3 -0
  105. package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
  106. package/corpus/templates/content/actions/delete-content-database.ts +12 -9
  107. package/corpus/templates/content/actions/delete-database-items.ts +3 -2
  108. package/corpus/templates/content/actions/delete-document.ts +233 -6
  109. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
  110. package/corpus/templates/content/actions/get-document.ts +4 -1
  111. package/corpus/templates/content/actions/list-content-databases.ts +1 -0
  112. package/corpus/templates/content/actions/list-documents.ts +1 -0
  113. package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
  114. package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
  115. package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
  116. package/corpus/templates/content/actions/pull-document.ts +15 -2
  117. package/corpus/templates/content/actions/restore-content-database.ts +48 -11
  118. package/corpus/templates/content/actions/restore-document.ts +28 -0
  119. package/corpus/templates/content/actions/search-documents.ts +2 -1
  120. package/corpus/templates/content/actions/view-screen.ts +2 -1
  121. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
  122. package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
  123. package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
  124. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
  125. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
  126. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
  127. package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
  128. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
  129. package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
  130. package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
  131. package/corpus/templates/content/app/i18n-data.ts +83 -2
  132. package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
  133. package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
  134. package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
  135. package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
  136. package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
  137. package/corpus/templates/content/parity/matrix.md +1 -1
  138. package/corpus/templates/content/parity/matrix.ts +3 -0
  139. package/corpus/templates/content/server/db/schema.ts +2 -0
  140. package/corpus/templates/content/server/lib/public-documents.ts +8 -2
  141. package/corpus/templates/content/server/plugins/db.ts +52 -0
  142. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
  143. package/corpus/templates/content/shared/api.ts +10 -0
  144. package/corpus/toolkit/CHANGELOG.md +6 -0
  145. package/corpus/toolkit/package.json +1 -1
  146. package/corpus/toolkit/src/ui/progress.tsx +3 -2
  147. package/dist/cli/mcp-config-writers.d.ts +2 -2
  148. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  149. package/dist/cli/mcp-config-writers.js +6 -6
  150. package/dist/cli/mcp-config-writers.js.map +1 -1
  151. package/dist/cli/mcp.d.ts +14 -0
  152. package/dist/cli/mcp.d.ts.map +1 -1
  153. package/dist/cli/mcp.js +79 -0
  154. package/dist/cli/mcp.js.map +1 -1
  155. package/dist/cli/skills-content/help.d.ts +1 -1
  156. package/dist/cli/skills-content/help.d.ts.map +1 -1
  157. package/dist/cli/skills-content/help.js +4 -3
  158. package/dist/cli/skills-content/help.js.map +1 -1
  159. package/dist/cli/skills-content/index.d.ts +1 -0
  160. package/dist/cli/skills-content/index.d.ts.map +1 -1
  161. package/dist/cli/skills-content/index.js +1 -0
  162. package/dist/cli/skills-content/index.js.map +1 -1
  163. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  164. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  165. package/dist/cli/skills-content/rewind-skill.js +53 -0
  166. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  167. package/dist/cli/skills.d.ts +6 -0
  168. package/dist/cli/skills.d.ts.map +1 -1
  169. package/dist/cli/skills.js +90 -14
  170. package/dist/cli/skills.js.map +1 -1
  171. package/dist/client/resources/ResourceTree.d.ts +3 -1
  172. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  173. package/dist/client/resources/ResourceTree.js +4 -4
  174. package/dist/client/resources/ResourceTree.js.map +1 -1
  175. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  176. package/dist/client/resources/ResourcesPanel.js +55 -6
  177. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  178. package/dist/collab/routes.d.ts +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  181. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  182. package/dist/mcp/screen-memory-stdio.js +1014 -91
  183. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  184. package/dist/mcp-client/index.d.ts +1 -1
  185. package/dist/mcp-client/index.d.ts.map +1 -1
  186. package/dist/mcp-client/index.js +1 -1
  187. package/dist/mcp-client/index.js.map +1 -1
  188. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  189. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  190. package/dist/mcp-client/screen-memory-local.js +590 -12
  191. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  192. package/dist/notifications/routes.d.ts +2 -2
  193. package/dist/observability/routes.d.ts +5 -5
  194. package/dist/progress/routes.d.ts +1 -1
  195. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  196. package/dist/resources/handlers.d.ts +1 -1
  197. package/dist/secrets/routes.d.ts +9 -9
  198. package/dist/server/transcribe-voice.d.ts +1 -1
  199. package/package.json +2 -2
  200. package/src/cli/mcp-config-writers.ts +6 -4
  201. package/src/cli/mcp.ts +124 -0
  202. package/src/cli/skills-content/help.ts +4 -3
  203. package/src/cli/skills-content/index.ts +1 -0
  204. package/src/cli/skills-content/rewind-skill.ts +52 -0
  205. package/src/cli/skills.ts +115 -13
  206. package/src/client/resources/ResourceTree.tsx +11 -5
  207. package/src/client/resources/ResourcesPanel.tsx +141 -18
  208. package/src/mcp/screen-memory-stdio.ts +1360 -104
  209. package/src/mcp-client/index.ts +9 -0
  210. 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
+ }