@mentra/glasses-media 3.4.0-dev.949 → 3.4.0-dev.951

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/index.d.ts CHANGED
@@ -7,5 +7,5 @@ export const glassesHotspot: {
7
7
  gateway?: string,
8
8
  purpose?: "outgoing-client" | "incoming-server",
9
9
  ): Promise<string>
10
- leave(id: string): Promise<void>
10
+ leave(id: string, waitForInternet?: boolean): Promise<void>
11
11
  }
package/index.js CHANGED
@@ -6,5 +6,5 @@ const native = () => require("expo-modules-core").requireNativeModule("MentraGla
6
6
  export const glassesHotspot = Object.freeze({
7
7
  join: (id, ssid, password, gateway, purpose = "outgoing-client") =>
8
8
  native().joinLocalHotspot(id, ssid, password, gateway ?? null, purpose),
9
- leave: (id) => native().leaveLocalHotspot(id),
9
+ leave: (id, waitForInternet = false) => native().leaveLocalHotspot(id, waitForInternet),
10
10
  })
@@ -8,9 +8,106 @@ public enum HotspotInternetPolicy {
8
8
  if cellular { return "cellular" }
9
9
  if ethernet { return "ethernet" }
10
10
  // Callers supply a Wi-Fi SSID only after releasing the glasses hotspot.
11
- if let restoredWifiSSID, !restoredWifiSSID.isEmpty, restoredWifiSSID != glassesSSID {
11
+ if let restoredWifiSSID, let glassesSSID, !glassesSSID.isEmpty, !restoredWifiSSID.isEmpty, restoredWifiSSID != glassesSSID {
12
12
  return "wifi"
13
13
  }
14
14
  return nil
15
15
  }
16
16
  }
17
+
18
+ /// One queue-confined route observation. Late SSID callbacks cannot settle a
19
+ /// newer path or session, and timeout/cancellation settle this observation once.
20
+ public final class HotspotInternetObservation {
21
+ private let generation: Int
22
+ private let glassesSSID: String?
23
+ private var pathRevision = 0
24
+ private var finished = false
25
+ private var readingSSID = false
26
+ private var settledReply: (() -> Void)?
27
+
28
+ public init(generation: Int, glassesSSID: String?) {
29
+ self.generation = generation
30
+ self.glassesSSID = glassesSSID
31
+ }
32
+
33
+ public func beginPath(currentGeneration: Int) -> Int? {
34
+ guard !finished, currentGeneration == generation else { return nil }
35
+ pathRevision += 1
36
+ return pathRevision
37
+ }
38
+
39
+ public func restoredWifiRoute(ssid: String?, pathRevision: Int, currentGeneration: Int) -> String? {
40
+ guard !finished, currentGeneration == generation, pathRevision == self.pathRevision else { return nil }
41
+ return HotspotInternetPolicy.route(satisfied: true, cellular: false, ethernet: false,
42
+ restoredWifiSSID: ssid, glassesSSID: glassesSSID)
43
+ }
44
+
45
+ public func beginSSIDRead() -> Bool {
46
+ guard !finished, !readingSSID else { return false }
47
+ readingSSID = true
48
+ return true
49
+ }
50
+
51
+ public func completeSSIDRead() {
52
+ readingSSID = false
53
+ let reply = settledReply
54
+ settledReply = nil
55
+ reply?()
56
+ }
57
+
58
+ public func afterSSIDRead(_ reply: @escaping () -> Void) {
59
+ if readingSSID { settledReply = reply } else { reply() }
60
+ }
61
+
62
+ public func settle() -> Bool {
63
+ guard !finished else { return false }
64
+ finished = true
65
+ return true
66
+ }
67
+ }
68
+
69
+ /// Queue-confined cancellation owner. An old observation may finish joining its
70
+ /// native read after its replacement starts, but cannot clear that replacement.
71
+ public final class HotspotInternetObservationOwner {
72
+ private var current: (observation: HotspotInternetObservation, cancel: () -> Void)?
73
+
74
+ public init() {}
75
+
76
+ public func install(_ observation: HotspotInternetObservation, cancel: @escaping () -> Void) {
77
+ current?.cancel()
78
+ current = (observation, cancel)
79
+ }
80
+
81
+ public func clear(_ observation: HotspotInternetObservation) {
82
+ if current?.observation === observation { current = nil }
83
+ }
84
+
85
+ public func cancel() {
86
+ current?.cancel()
87
+ }
88
+ }
89
+
90
+ /// Queue-confined completion of one leave. Concurrent callers share cleanup;
91
+ /// the original ownership token is released before any caller can start again.
92
+ public final class HotspotLeaveCompletion {
93
+ private var replies: [(Result<Void, Error>) -> Void] = []
94
+ private var pending = true
95
+ private let release: () -> Void
96
+
97
+ public init(release: @escaping () -> Void) {
98
+ self.release = release
99
+ }
100
+
101
+ public func append(_ reply: @escaping (Result<Void, Error>) -> Void) {
102
+ replies.append(reply)
103
+ }
104
+
105
+ public func complete(_ result: Result<Void, Error>) {
106
+ guard pending else { return }
107
+ pending = false
108
+ if case .success = result { release() }
109
+ let current = replies
110
+ replies.removeAll()
111
+ current.forEach { $0(result) }
112
+ }
113
+ }
@@ -5,13 +5,23 @@ import ObjectiveC
5
5
  /// Blocking public CoreWLAN calls are isolated here; the existing hotspot owner
6
6
  /// retains custody until they return, even after cancellation or a timeout.
7
7
  final class MacHotspotAssociation {
8
- private let lock = NSLock()
8
+ private let condition = NSCondition()
9
9
  private var cancelled = false
10
10
  private var interface: MacWifiInterface?
11
11
  private var joinedSSID: String?
12
+ private let deadline: TimeInterval
13
+ private let now: () -> TimeInterval
14
+ private let pause: ((TimeInterval) -> Void)?
12
15
  private let makeInterface: () throws -> MacWifiInterface
13
16
 
14
- init(makeInterface: @escaping () throws -> MacWifiInterface = { try PublicCoreWlanInterface() }) {
17
+ init(deadline: TimeInterval = ProcessInfo.processInfo.systemUptime + 60,
18
+ now: @escaping () -> TimeInterval = { ProcessInfo.processInfo.systemUptime },
19
+ pause: ((TimeInterval) -> Void)? = nil,
20
+ makeInterface: @escaping () throws -> MacWifiInterface = { try PublicCoreWlanInterface() })
21
+ {
22
+ self.deadline = deadline
23
+ self.now = now
24
+ self.pause = pause
15
25
  self.makeInterface = makeInterface
16
26
  }
17
27
 
@@ -20,12 +30,15 @@ final class MacHotspotAssociation {
20
30
  let interface = try makeInterface()
21
31
  self.interface = interface
22
32
  if try interface.currentSSID() != ssid {
23
- let network = try interface.network(ssid: ssid)
24
- try checkCurrent()
25
- // Record the intended association before this blocking call. A late
26
- // successful join still belongs to us and must be cleaned up.
27
- joinedSSID = ssid
28
- try interface.associate(network: network, password: password)
33
+ let network = try discoverNetwork(ssid: ssid, interface: interface)
34
+ try checkDiscoveryCurrent()
35
+ if let network, try interface.currentSSID() != ssid {
36
+ try checkDiscoveryCurrent()
37
+ // Record the intended association before this blocking call. A late
38
+ // successful join still belongs to us and must be cleaned up.
39
+ joinedSSID = ssid
40
+ try interface.associate(network: network, password: password)
41
+ }
29
42
  }
30
43
  try checkCurrent()
31
44
  guard try interface.currentSSID() == ssid else {
@@ -35,7 +48,7 @@ final class MacHotspotAssociation {
35
48
  }
36
49
 
37
50
  func cancel() {
38
- lock.lock(); cancelled = true; lock.unlock()
51
+ condition.lock(); cancelled = true; condition.broadcast(); condition.unlock()
39
52
  }
40
53
 
41
54
  /// Run on the same worker as association, after its blocking call returns.
@@ -57,8 +70,40 @@ final class MacHotspotAssociation {
57
70
  try interface?.currentSSID()
58
71
  }
59
72
 
73
+ static func observeCurrentSSID() throws -> String? {
74
+ try PublicCoreWlanInterface().currentSSID()
75
+ }
76
+
77
+ private func discoverNetwork(ssid: String, interface: MacWifiInterface) throws -> NSObject? {
78
+ while true {
79
+ try checkDiscoveryCurrent()
80
+ // Recheck Location authorization before and after each blocking scan.
81
+ if try interface.currentSSID() == ssid { return nil }
82
+ let network = try interface.network(ssid: ssid)
83
+ try checkDiscoveryCurrent()
84
+ if try interface.currentSSID() == ssid { return nil }
85
+ if let network { return network }
86
+ // Successful empty scans remain pending within this same join window.
87
+ // Native scan errors escape immediately; association is never retried.
88
+ let remaining = max(0, deadline - now())
89
+ if let pause { pause(min(1, remaining)) }
90
+ else {
91
+ condition.lock()
92
+ if !cancelled { _ = condition.wait(until: Date(timeIntervalSinceNow: min(1, remaining))) }
93
+ condition.unlock()
94
+ }
95
+ }
96
+ }
97
+
98
+ private func checkDiscoveryCurrent() throws {
99
+ try checkCurrent()
100
+ guard now() < deadline else {
101
+ throw LocalMediaError("The requested glasses hotspot was not found by Mac Wi-Fi before the hotspot join timed out")
102
+ }
103
+ }
104
+
60
105
  private func checkCurrent() throws {
61
- lock.lock(); let stopped = cancelled; lock.unlock()
106
+ condition.lock(); let stopped = cancelled; condition.unlock()
62
107
  if stopped { throw CancellationError() }
63
108
  }
64
109
  }
@@ -66,7 +111,7 @@ final class MacHotspotAssociation {
66
111
  protocol MacWifiInterface: AnyObject {
67
112
  func name() throws -> String
68
113
  func currentSSID() throws -> String?
69
- func network(ssid: String) throws -> NSObject
114
+ func network(ssid: String) throws -> NSObject?
70
115
  func associate(network: NSObject, password: String) throws
71
116
  func disassociate()
72
117
  }
@@ -122,7 +167,7 @@ private final class PublicCoreWlanInterface: MacWifiInterface {
122
167
  return String(data: data, encoding: .utf8)
123
168
  }
124
169
 
125
- func network(ssid: String) throws -> NSObject {
170
+ func network(ssid: String) throws -> NSObject? {
126
171
  let selector = NSSelectorFromString("scanForNetworksWithSSID:error:")
127
172
  let invoke = unsafeBitCast(interface.method(for: selector), to: MacWifiInvocation.Scan.self)
128
173
  var error: NSError?
@@ -137,7 +182,7 @@ private final class PublicCoreWlanInterface: MacWifiInterface {
137
182
  data == Data(ssid.utf8) else { continue }
138
183
  return network
139
184
  }
140
- throw LocalMediaError("The requested glasses hotspot was not found by Mac Wi-Fi")
185
+ return nil
141
186
  }
142
187
 
143
188
  func associate(network: NSObject, password: String) throws {
@@ -19,7 +19,7 @@ public final class GlassesHotspotNetwork {
19
19
  private var ownsConfiguration = false
20
20
  private var cancelled = false
21
21
  private var joinReply: ((Result<String, Error>) -> Void)?
22
- private var leaveReplies: [(Result<Void, Error>) -> Void] = []
22
+ private var leaveCompletion: HotspotLeaveCompletion?
23
23
  private var interfaceMonitors: [NWPathMonitor] = []
24
24
  /// Wi-Fi identity learned since this join started; replaced on every join and cleared on leave.
25
25
  private var wifiIdentity = HotspotWifiIdentity()
@@ -33,6 +33,8 @@ public final class GlassesHotspotNetwork {
33
33
  private var macAuthorization: MacHotspotAuthorization?
34
34
  private var ownershipToken: UUID?
35
35
  private var finishingLeave = false
36
+ private var waitForInternetAfterLeave = false
37
+ private let internetObservationOwner = HotspotInternetObservationOwner()
36
38
  public var onLost: ((String) -> Void)?
37
39
  public var onPermissionRequired: (() -> Void)?
38
40
  public init() {}
@@ -52,6 +54,7 @@ public final class GlassesHotspotNetwork {
52
54
  self.ownershipToken = token
53
55
  self.generation += 1
54
56
  let gen = self.generation
57
+ let deadline = ProcessInfo.processInfo.systemUptime + 60
55
58
  self.ssid = ssid
56
59
  self.lastHotspotSSID = ssid
57
60
  self.gatewayAddress = gateway
@@ -62,11 +65,11 @@ public final class GlassesHotspotNetwork {
62
65
  self.joinReply = completion
63
66
  self.startInterfaceMonitors(generation: gen)
64
67
  if ProcessInfo.processInfo.isiOSAppOnMac {
65
- self.authorizeMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen)
68
+ self.authorizeMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen, deadline: deadline)
66
69
  } else {
67
70
  self.applyConfiguration(ssid: ssid, passphrase: passphrase, generation: gen)
68
71
  }
69
- self.queue.asyncAfter(deadline: .now() + 60) {
72
+ self.queue.asyncAfter(deadline: .now() + max(0, deadline - ProcessInfo.processInfo.systemUptime)) {
70
73
  // Once associated, the user may still be reading the Local Network alert.
71
74
  // Its response time is not a hotspot association timeout.
72
75
  guard gen == self.generation, self.joinReply != nil, self.localAccess == nil else { return }
@@ -82,12 +85,20 @@ public final class GlassesHotspotNetwork {
82
85
  }
83
86
  }
84
87
 
85
- public func leave(completion: @escaping (Result<Void, Error>) -> Void) {
88
+ public func leave(waitForInternet: Bool = false, completion: @escaping (Result<Void, Error>) -> Void) {
86
89
  queue.async {
90
+ if !waitForInternet { self.internetObservationOwner.cancel() }
91
+ self.waitForInternetAfterLeave = self.waitForInternetAfterLeave || waitForInternet
87
92
  self.cancelled = true
88
93
  self.macAssociation?.cancel()
89
94
  self.macAuthorization?.cancel()
90
- self.leaveReplies.append(completion)
95
+ if self.leaveCompletion == nil {
96
+ self.leaveCompletion = HotspotLeaveCompletion {
97
+ if let token = self.ownershipToken { HotspotNetworkOwnership.shared.release(token) }
98
+ self.ownershipToken = nil
99
+ }
100
+ }
101
+ self.leaveCompletion?.append(completion)
91
102
  self.finishJoin(.failure(LocalMediaError("Hotspot join cancelled")))
92
103
  if let ssid = self.ssid, self.ownsConfiguration { NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid) }
93
104
  if !self.applying { self.finishLeave() }
@@ -157,44 +168,77 @@ public final class GlassesHotspotNetwork {
157
168
  }
158
169
 
159
170
  public func awaitInternet(allowWifiAfterRelease: Bool = false, completion: @escaping (Bool, String) -> Void) {
160
- queue.async {
161
- let monitor = NWPathMonitor()
162
- var finished = false
163
- let finish: (Bool, String) -> Void = { usable, detail in
164
- guard !finished else { return }
165
- finished = true
166
- monitor.cancel()
171
+ queue.async { self.startInternetObservation(allowWifiAfterRelease: allowWifiAfterRelease, completion: completion) }
172
+ }
173
+
174
+ /// Called on the owner queue, including directly from completeLeave. A queued
175
+ /// cancellation therefore cannot overtake observation initialization.
176
+ private func startInternetObservation(allowWifiAfterRelease: Bool, completion: @escaping (Bool, String) -> Void) {
177
+ let gen = generation
178
+ let observation = HotspotInternetObservation(generation: gen, glassesSSID: lastHotspotSSID)
179
+ let monitor = NWPathMonitor()
180
+ let sampleTimer = DispatchSource.makeTimerSource(queue: queue)
181
+ var currentPath: Network.NWPath?
182
+ let finish: (Bool, String) -> Void = { usable, detail in
183
+ guard observation.settle() else { return }
184
+ monitor.pathUpdateHandler = nil
185
+ monitor.cancel()
186
+ sampleTimer.setEventHandler {}
187
+ sampleTimer.cancel()
188
+ currentPath = nil
189
+ let reply = {
190
+ self.internetObservationOwner.clear(observation)
167
191
  completion(usable, detail)
168
192
  }
169
- monitor.pathUpdateHandler = { path in
170
- if let route = HotspotInternetPolicy.route(satisfied: path.status == .satisfied,
171
- cellular: path.usesInterfaceType(.cellular),
172
- ethernet: path.usesInterfaceType(.wiredEthernet))
173
- {
193
+ // Even a timed-out/cancelled observation joins its dispatched
194
+ // CoreWLAN read before releasing this session's owner.
195
+ observation.afterSSIDRead(reply)
196
+ }
197
+ internetObservationOwner.install(observation) { finish(false, "Hotspot internet observation cancelled") }
198
+ var currentPathRevision = 0
199
+ let sample = {
200
+ guard let path = currentPath else { return }
201
+ if let route = HotspotInternetPolicy.route(satisfied: path.status == .satisfied,
202
+ cellular: path.usesInterfaceType(.cellular),
203
+ ethernet: path.usesInterfaceType(.wiredEthernet))
204
+ {
205
+ finish(true, route)
206
+ return
207
+ }
208
+ // Sample returned membership while the route remains on Wi-Fi;
209
+ // NWPath need not report a new interface when the SSID changes.
210
+ guard allowWifiAfterRelease, path.status == .satisfied,
211
+ path.usesInterfaceType(.wifi), observation.beginSSIDRead() else { return }
212
+ let pathRevision = currentPathRevision
213
+ let observed: (String?) -> Void = { ssid in
214
+ self.queue.async {
215
+ observation.completeSSIDRead()
216
+ guard gen == self.generation else { finish(false, "Hotspot session changed"); return }
217
+ guard let route = observation.restoredWifiRoute(ssid: ssid, pathRevision: pathRevision, currentGeneration: self.generation)
218
+ else { return }
174
219
  finish(true, route)
175
- return
176
- }
177
- // Once the hotspot is released, a return to the user's Wi-Fi is also valid.
178
- // Do not mistake the departing glasses AP's local-only path for restored internet.
179
- if allowWifiAfterRelease, path.status == .satisfied, path.usesInterfaceType(.wifi) {
180
- NEHotspotNetwork.fetchCurrent { network in
181
- self.queue.async {
182
- guard let route = HotspotInternetPolicy.route(satisfied: true, cellular: false, ethernet: false,
183
- restoredWifiSSID: network?.ssid, glassesSSID: self.lastHotspotSSID)
184
- else { return }
185
- finish(true, route)
186
- }
187
- }
188
220
  }
189
221
  }
190
- monitor.start(queue: self.queue)
191
- self.queue.asyncAfter(deadline: .now() + 15) {
192
- finish(false, allowWifiAfterRelease ? "Internet did not return after leaving the glasses hotspot" : "Cellular or Ethernet internet did not become the default route")
193
- }
222
+ if ProcessInfo.processInfo.isiOSAppOnMac {
223
+ self.macWorker.async { observed(try? MacHotspotAssociation.observeCurrentSSID()) }
224
+ } else { NEHotspotNetwork.fetchCurrent { observed($0?.ssid) } }
225
+ }
226
+ monitor.pathUpdateHandler = { path in
227
+ guard let pathRevision = observation.beginPath(currentGeneration: self.generation) else { finish(false, "Hotspot session changed"); return }
228
+ currentPath = path
229
+ currentPathRevision = pathRevision
230
+ sample()
231
+ }
232
+ sampleTimer.setEventHandler(handler: sample)
233
+ sampleTimer.schedule(deadline: .now() + 0.5, repeating: 0.5)
234
+ sampleTimer.resume()
235
+ monitor.start(queue: queue)
236
+ queue.asyncAfter(deadline: .now() + 15) {
237
+ finish(false, allowWifiAfterRelease ? "Internet did not return after leaving the glasses hotspot" : "Cellular or Ethernet internet did not become the default route")
194
238
  }
195
239
  }
196
240
 
197
- private func authorizeMacAssociation(ssid: String, passphrase: String, generation gen: Int) {
241
+ private func authorizeMacAssociation(ssid: String, passphrase: String, generation gen: Int, deadline: TimeInterval) {
198
242
  let authorization = MacHotspotAuthorization()
199
243
  macAuthorization = authorization
200
244
  authorization.start { result in
@@ -202,7 +246,7 @@ public final class GlassesHotspotNetwork {
202
246
  guard gen == self.generation, !self.cancelled else { return }
203
247
  self.macAuthorization = nil
204
248
  switch result {
205
- case .success: self.applyMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen)
249
+ case .success: self.applyMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen, deadline: deadline)
206
250
  case let .failure(error):
207
251
  self.finishJoin(.failure(error)); self.finishLeave()
208
252
  }
@@ -210,8 +254,8 @@ public final class GlassesHotspotNetwork {
210
254
  }
211
255
  }
212
256
 
213
- private func applyMacAssociation(ssid: String, passphrase: String, generation gen: Int) {
214
- let association = MacHotspotAssociation()
257
+ private func applyMacAssociation(ssid: String, passphrase: String, generation gen: Int, deadline: TimeInterval) {
258
+ let association = MacHotspotAssociation(deadline: deadline)
215
259
  macAssociation = association
216
260
  applying = true
217
261
  macWorker.async {
@@ -427,9 +471,9 @@ public final class GlassesHotspotNetwork {
427
471
  // Retain the owner when we cannot prove which association
428
472
  // cleanup would disconnect. A later leave can retry safely.
429
473
  self.finishingLeave = false
430
- let replies = self.leaveReplies
431
- self.leaveReplies.removeAll()
432
- replies.forEach { $0(.failure(error)) }
474
+ let completion = self.leaveCompletion
475
+ self.leaveCompletion = nil
476
+ completion?.complete(.failure(error))
433
477
  }
434
478
  }
435
479
  }
@@ -437,6 +481,7 @@ public final class GlassesHotspotNetwork {
437
481
  }
438
482
 
439
483
  private func completeLeave() {
484
+ internetObservationOwner.cancel()
440
485
  generation += 1
441
486
  macAuthorization?.cancel(); macAuthorization = nil
442
487
  localAccess?.cancel(); localAccess = nil
@@ -448,12 +493,25 @@ public final class GlassesHotspotNetwork {
448
493
  ssid = nil
449
494
  binding = nil
450
495
  gatewayAddress = nil
451
- finishingLeave = false
452
- if let token = ownershipToken { HotspotNetworkOwnership.shared.release(token) }
453
- ownershipToken = nil
454
- let replies = leaveReplies
455
- leaveReplies.removeAll()
456
- replies.forEach { $0(.success(())) }
496
+ let finish = {
497
+ self.waitForInternetAfterLeave = false
498
+ self.finishingLeave = false
499
+ let completion = self.leaveCompletion
500
+ self.leaveCompletion = nil
501
+ if let completion { completion.complete(.success(())) } else {
502
+ if let token = self.ownershipToken { HotspotNetworkOwnership.shared.release(token) }
503
+ self.ownershipToken = nil
504
+ }
505
+ }
506
+ if waitForInternetAfterLeave, ProcessInfo.processInfo.isiOSAppOnMac {
507
+ // Keep this owner until the default route returns or the existing
508
+ // bounded observation ends. Route readiness is not HTTP success;
509
+ // the following ordinary update check still owns that result.
510
+ startInternetObservation(allowWifiAfterRelease: true) { ready, reason in
511
+ self.logger.info("HOTSPOT_LEAVE internet_recovery ready=\(ready) reason=\(reason, privacy: .public)")
512
+ finish()
513
+ }
514
+ } else { finish() }
457
515
  }
458
516
 
459
517
  /// Every attached interface's kernel index and link type (its AF_LINK row) and every IPv4
@@ -31,10 +31,10 @@ public final class GlassesMediaRelayModule: Module {
31
31
  }
32
32
  }
33
33
 
34
- AsyncFunction("leaveLocalHotspot") { (id: String, promise: Promise) in
34
+ AsyncFunction("leaveLocalHotspot") { (id: String, waitForInternet: Bool, promise: Promise) in
35
35
  self.queue.async {
36
36
  guard self.localHotspotOwner == id else { promise.resolve(nil); return }
37
- self.localHotspot.leave { result in
37
+ self.localHotspot.leave(waitForInternet: waitForInternet) { result in
38
38
  self.queue.async {
39
39
  switch result {
40
40
  case .success:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/glasses-media",
3
- "version": "3.4.0-dev.949",
3
+ "version": "3.4.0-dev.951",
4
4
  "description": "Shared native glasses media receivers and local network transport",
5
5
  "license": "MIT",
6
6
  "author": "Mentra Community",