1use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_lib_stream_core::{
5 ClockDomain, LatencyClass, PlacedFragment, StreamEdge, StreamEnvelope, StreamMedia,
6};
7
8use crate::transport::TransportKind;
9
10pub fn browser_wasm_site_symbol() -> Symbol {
12 Symbol::qualified("stream/site", "browser-wasm")
13}
14
15pub fn browser_wasm_engine_entry_symbol() -> Symbol {
17 Symbol::qualified("stream/wasm-entry", "browser-wasm-engine")
18}
19
20pub fn browser_audio_worklet_entry_symbol() -> Symbol {
22 Symbol::qualified("stream/wasm-entry", "browser-audio-worklet")
23}
24
25pub fn browser_server_only_refusal_diagnostic() -> Symbol {
27 Symbol::qualified("stream/browser-diagnostic", "server-only-refused")
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum BrowserBridgeLane {
33 Ui,
35 Preview,
37 Trace,
39}
40
41impl BrowserBridgeLane {
42 pub fn symbol(self) -> Symbol {
44 match self {
45 Self::Ui => Symbol::qualified("stream/browser-bridge", "ui"),
46 Self::Preview => Symbol::qualified("stream/browser-bridge", "preview"),
47 Self::Trace => Symbol::qualified("stream/browser-bridge", "trace"),
48 }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct BrowserWasmEntryPoints {
55 engine: Symbol,
56 audio_worklet: Symbol,
57}
58
59impl BrowserWasmEntryPoints {
60 pub fn browser_defaults() -> Self {
62 Self {
63 engine: browser_wasm_engine_entry_symbol(),
64 audio_worklet: browser_audio_worklet_entry_symbol(),
65 }
66 }
67
68 pub fn engine(&self) -> &Symbol {
70 &self.engine
71 }
72
73 pub fn audio_worklet(&self) -> &Symbol {
75 &self.audio_worklet
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct BrowserWasmEngine {
82 id: Symbol,
83 entry_points: BrowserWasmEntryPoints,
84 audio_worklet_capable: bool,
85}
86
87impl BrowserWasmEngine {
88 pub fn browser_local(id: Symbol) -> Self {
90 Self {
91 id,
92 entry_points: BrowserWasmEntryPoints::browser_defaults(),
93 audio_worklet_capable: true,
94 }
95 }
96
97 pub fn id(&self) -> &Symbol {
99 &self.id
100 }
101
102 pub fn entry_points(&self) -> &BrowserWasmEntryPoints {
104 &self.entry_points
105 }
106
107 pub fn audio_worklet_capable(&self) -> bool {
109 self.audio_worklet_capable
110 }
111
112 pub fn transport_kind(&self) -> TransportKind {
114 TransportKind::Wasm
115 }
116
117 pub fn uses_server_audio_tunnel(&self) -> bool {
119 false
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct BrowserPlacementRequest {
126 fragment: PlacedFragment,
127 engine: BrowserWasmEngine,
128 server_only: bool,
129}
130
131impl BrowserPlacementRequest {
132 pub fn new(fragment: PlacedFragment, engine: BrowserWasmEngine) -> Self {
134 Self {
135 fragment,
136 engine,
137 server_only: false,
138 }
139 }
140
141 pub fn with_server_only(mut self, server_only: bool) -> Self {
143 self.server_only = server_only;
144 self
145 }
146
147 pub fn run_headless(&self) -> Result<BrowserPlacementReport> {
154 if self.server_only {
155 let diagnostic = browser_server_only_refusal_diagnostic();
156 return Err(Error::Eval(format!(
157 "{}: server-only nodes cannot run in browser-wasm placement",
158 diagnostic.as_qualified_str()
159 )));
160 }
161
162 Ok(BrowserPlacementReport {
163 fragment_id: self.fragment.id().clone(),
164 site: browser_wasm_site_symbol(),
165 engine: self.engine.clone(),
166 lanes: carried_lanes(&self.fragment),
167 output_envelopes: self.fragment.output_envelopes(),
168 diagnostics: Vec::new(),
169 })
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct BrowserPlacementReport {
176 fragment_id: Symbol,
177 site: Symbol,
178 engine: BrowserWasmEngine,
179 lanes: Vec<BrowserBridgeLane>,
180 output_envelopes: Vec<StreamEnvelope>,
181 diagnostics: Vec<Symbol>,
182}
183
184impl BrowserPlacementReport {
185 pub fn fragment_id(&self) -> &Symbol {
187 &self.fragment_id
188 }
189
190 pub fn site(&self) -> &Symbol {
192 &self.site
193 }
194
195 pub fn engine(&self) -> &BrowserWasmEngine {
197 &self.engine
198 }
199
200 pub fn lanes(&self) -> &[BrowserBridgeLane] {
202 &self.lanes
203 }
204
205 pub fn output_envelopes(&self) -> &[StreamEnvelope] {
207 &self.output_envelopes
208 }
209
210 pub fn diagnostics(&self) -> &[Symbol] {
212 &self.diagnostics
213 }
214
215 pub fn to_expr(&self) -> Expr {
217 Expr::Map(vec![
218 (
219 Expr::Symbol(Symbol::new("fragment")),
220 Expr::Symbol(self.fragment_id.clone()),
221 ),
222 (
223 Expr::Symbol(Symbol::new("site")),
224 Expr::Symbol(self.site.clone()),
225 ),
226 (
227 Expr::Symbol(Symbol::new("transport")),
228 Expr::Symbol(Symbol::qualified("stream/transport", "wasm")),
229 ),
230 (
231 Expr::Symbol(Symbol::new("engine")),
232 Expr::Symbol(self.engine.id().clone()),
233 ),
234 (
235 Expr::Symbol(Symbol::new("entry-points")),
236 Expr::List(vec![
237 Expr::Symbol(self.engine.entry_points().engine().clone()),
238 Expr::Symbol(self.engine.entry_points().audio_worklet().clone()),
239 ]),
240 ),
241 (
242 Expr::Symbol(Symbol::new("lanes")),
243 Expr::List(
244 self.lanes
245 .iter()
246 .map(|lane| Expr::Symbol(lane.symbol()))
247 .collect(),
248 ),
249 ),
250 (
251 Expr::Symbol(Symbol::new("diagnostics")),
252 Expr::List(self.diagnostics.iter().cloned().map(Expr::Symbol).collect()),
253 ),
254 ])
255 }
256}
257
258fn carried_lanes(fragment: &PlacedFragment) -> Vec<BrowserBridgeLane> {
259 let mut lanes = Vec::new();
260 for lane in fragment
261 .input_edges()
262 .iter()
263 .chain(fragment.output_edges())
264 .filter_map(lane_for_edge)
265 {
266 if !lanes.contains(&lane) {
267 lanes.push(lane);
268 }
269 }
270 lanes
271}
272
273fn lane_for_edge(edge: &StreamEdge) -> Option<BrowserBridgeLane> {
274 let rate = edge.rate_contract();
275 if rate.clock_domain() == ClockDomain::TraceStep {
276 return Some(BrowserBridgeLane::Trace);
277 }
278 if edge.metadata().media() == StreamMedia::Pcm
279 && rate.latency_class() == LatencyClass::BufferedPreview
280 {
281 return Some(BrowserBridgeLane::Preview);
282 }
283 if rate.clock_domain() == ClockDomain::BrowserFrame
284 || edge.metadata().media() == StreamMedia::Data
285 {
286 return Some(BrowserBridgeLane::Ui);
287 }
288 None
289}