@mentra/bluetooth-sdk 3.2.0-dev.203 → 3.2.0-dev.208
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/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +0 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +13 -152
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +6 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +1 -3
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +0 -11
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +10 -130
- package/ios/Source/sgcs/MentraLive.swift +46 -35
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +6 -1
- package/src/_private/BluetoothSdkModule.ts +0 -3
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -807,16 +807,8 @@ class BluetoothSdkModule : Module() {
|
|
|
807
807
|
requireSdk().startStream(StreamRequest.fromMap(params)).values
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
-
SdkCoroutineFunction("startExternallyManagedStream") { params: Map<String, Any> ->
|
|
811
|
-
requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
|
|
812
|
-
}
|
|
813
|
-
|
|
814
810
|
SdkCoroutineFunction("stopStream") { -> requireSdk().stopStream().values }
|
|
815
811
|
|
|
816
|
-
SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
|
|
817
|
-
sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
818
|
-
}
|
|
819
|
-
|
|
820
812
|
// MARK: - Microphone Commands
|
|
821
813
|
|
|
822
814
|
SdkAsyncFunction("setMicState") {
|
|
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "3.2.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.2.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.2.0-dev.208"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.208"
|
|
8
|
+
const val SOURCE_COMMIT: String = "3c22b92a5a53ce608c087503417ca5b3d16cc926"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.208.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "69ce2b786afea60faaa5a47aad1ff0c9e70933365658b6628e651fb0c6cfe7bd"
|
|
11
11
|
}
|
|
@@ -8,6 +8,7 @@ import android.content.Context
|
|
|
8
8
|
import android.os.Bundle
|
|
9
9
|
import android.os.Handler
|
|
10
10
|
import android.os.Looper
|
|
11
|
+
import com.mentra.bluetoothsdk.streaming.StreamSessionState
|
|
11
12
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
12
13
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
13
14
|
import java.util.Collections
|
|
@@ -52,8 +53,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
52
53
|
}
|
|
53
54
|
private var activityLifecycleCallbacksRegistered = false
|
|
54
55
|
private var suppressDefaultDeviceEvents = false
|
|
55
|
-
private val
|
|
56
|
-
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
56
|
+
private val streamSession = StreamSessionState()
|
|
57
57
|
private val pendingPhotoRequests = ConcurrentHashMap<String, PendingResponse<PhotoResponseEvent>>()
|
|
58
58
|
private val pendingCameraStatusRequests = ConcurrentHashMap<String, PendingResponse<CameraStatusEvent>>()
|
|
59
59
|
private val pendingVideoRecordingRequests =
|
|
@@ -110,7 +110,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
110
110
|
private const val OTA_BES_VERSION_WAIT_MS = 5_000L
|
|
111
111
|
private const val OTA_MTK_VERSION_WAIT_MS = 2_000L
|
|
112
112
|
private const val OTA_VERSION_POLL_MS = 100L
|
|
113
|
-
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
114
113
|
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
115
114
|
|
|
116
115
|
// Stream states the glasses only reach after start_stream has run past
|
|
@@ -139,18 +138,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
139
138
|
): MentraBluetoothSdk = MentraBluetoothSdk(context, config, listener)
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
private data class ActiveStreamKeepAlive(
|
|
143
|
-
val streamId: String,
|
|
144
|
-
val intervalMs: Long,
|
|
145
|
-
var pendingAckId: String? = null,
|
|
146
|
-
var missedAckCount: Int = 0,
|
|
147
|
-
var nextTick: Runnable? = null,
|
|
148
|
-
// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a
|
|
149
|
-
// slow startup (glasses can't ACK until they reach starting/streaming) can't trip a
|
|
150
|
-
// false keep-alive timeout before the stream is ever up.
|
|
151
|
-
var armed: Boolean = false,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
141
|
// seq records send order (assigned and handed to the BLE queue under
|
|
155
142
|
// streamStartOrderLock) so that an id-carrying status for a newer start can
|
|
156
143
|
// identify which older in-flight starts it preempted.
|
|
@@ -1115,16 +1102,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1115
1102
|
}
|
|
1116
1103
|
}
|
|
1117
1104
|
|
|
1118
|
-
suspend fun startStream(request: StreamRequest): StreamStatusEvent
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
private suspend fun startStream(
|
|
1125
|
-
request: StreamRequest,
|
|
1126
|
-
startSdkKeepAlive: Boolean,
|
|
1127
|
-
): StreamStatusEvent {
|
|
1105
|
+
suspend fun startStream(request: StreamRequest): StreamStatusEvent {
|
|
1106
|
+
requireGlassesConnected("start stream")
|
|
1107
|
+
if (!streamSession.supported) {
|
|
1108
|
+
throw BluetoothSdkException("stream_control_unsupported", "Update the glasses software before starting a stream.")
|
|
1109
|
+
}
|
|
1128
1110
|
val message = request.toMap().toMutableMap()
|
|
1129
1111
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
1130
1112
|
?: "sdk-${UUID.randomUUID()}"
|
|
@@ -1139,23 +1121,15 @@ class MentraBluetoothSdk private constructor(
|
|
|
1139
1121
|
val registered = PendingStreamStart(streamStartSeq.incrementAndGet(), pending)
|
|
1140
1122
|
pendingStreamStarts[streamId] = registered
|
|
1141
1123
|
start = registered
|
|
1142
|
-
stopStreamKeepAliveMonitor()
|
|
1143
1124
|
deviceManager.startStream(message)
|
|
1144
1125
|
}
|
|
1145
1126
|
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
1146
|
-
if (startSdkKeepAlive) {
|
|
1147
|
-
startStreamKeepAliveMonitor(streamId, DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS)
|
|
1148
|
-
}
|
|
1149
1127
|
return event
|
|
1150
1128
|
} finally {
|
|
1151
1129
|
start?.let { pendingStreamStarts.remove(streamId, it) }
|
|
1152
1130
|
}
|
|
1153
1131
|
}
|
|
1154
1132
|
|
|
1155
|
-
internal fun sendExternallyManagedStreamKeepAlive(request: StreamKeepAliveRequest) {
|
|
1156
|
-
deviceManager.keepStreamAlive(request.toMap().toMutableMap())
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1133
|
suspend fun rgbLedControl(request: RgbLedRequest): RgbLedControlResponseEvent {
|
|
1160
1134
|
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}")
|
|
1161
1135
|
pendingRgbLedRequests[request.requestId] = pending
|
|
@@ -1177,9 +1151,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1177
1151
|
|
|
1178
1152
|
suspend fun stopStream(): StreamStatusEvent {
|
|
1179
1153
|
val pending = PendingResponse<StreamStatusEvent>("stop stream")
|
|
1180
|
-
val targetStreamId =
|
|
1181
|
-
activeStreamKeepAlive?.streamId
|
|
1182
|
-
}
|
|
1154
|
+
val targetStreamId = streamSession.currentStreamId
|
|
1183
1155
|
try {
|
|
1184
1156
|
// The seq draw and the BLE hand-off share startStream's ordering
|
|
1185
1157
|
// critical section: the stop takes its own slot in the send order,
|
|
@@ -1196,7 +1168,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1196
1168
|
pendingStreamStop =
|
|
1197
1169
|
PendingStreamStop(targetStreamId, streamStartSeq.incrementAndGet(), pending)
|
|
1198
1170
|
}
|
|
1199
|
-
stopStreamKeepAliveMonitor()
|
|
1200
1171
|
deviceManager.stopStream()
|
|
1201
1172
|
}
|
|
1202
1173
|
return pending.await(STREAM_STOP_TIMEOUT_MS)
|
|
@@ -1554,7 +1525,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1554
1525
|
}
|
|
1555
1526
|
|
|
1556
1527
|
override fun close() {
|
|
1557
|
-
stopStreamKeepAliveMonitor()
|
|
1558
1528
|
resetWifiProtocolSession("", "sdk_closed")
|
|
1559
1529
|
if (activityLifecycleCallbacksRegistered) {
|
|
1560
1530
|
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
@@ -1871,17 +1841,18 @@ class MentraBluetoothSdk private constructor(
|
|
|
1871
1841
|
handleRgbLedResponseForRequests(event)
|
|
1872
1842
|
dispatchToListeners { it.onRgbLedControlResponse(event) }
|
|
1873
1843
|
}
|
|
1844
|
+
"stream_control_ready" -> {
|
|
1845
|
+
streamSession.ready(data["sid"] as? String, (data["streamControlVersion"] as? Number)?.toInt())
|
|
1846
|
+
}
|
|
1874
1847
|
"stream_status" -> {
|
|
1848
|
+
if (data.containsKey("revision") && !streamSession.accept(data)) return
|
|
1875
1849
|
val event = StreamStatusEvent(data)
|
|
1876
1850
|
handleStreamStatusForRequests(event)
|
|
1877
|
-
handleStreamStatusForKeepAlive(event.status)
|
|
1878
1851
|
dispatchToListeners { it.onStreamStatus(event) }
|
|
1879
1852
|
}
|
|
1880
1853
|
"keep_alive_ack" -> {
|
|
1881
1854
|
val event = KeepAliveAckEvent(data)
|
|
1882
|
-
|
|
1883
|
-
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1884
|
-
}
|
|
1855
|
+
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1885
1856
|
}
|
|
1886
1857
|
"ota_start_ack" -> {
|
|
1887
1858
|
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
@@ -1974,29 +1945,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1974
1945
|
}
|
|
1975
1946
|
}
|
|
1976
1947
|
|
|
1977
|
-
private fun startStreamKeepAliveMonitor(streamId: String, requestedIntervalSeconds: Int) {
|
|
1978
|
-
val intervalSeconds =
|
|
1979
|
-
requestedIntervalSeconds.takeIf { it > 0 } ?: DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS
|
|
1980
|
-
val tracker = ActiveStreamKeepAlive(
|
|
1981
|
-
streamId = streamId,
|
|
1982
|
-
intervalMs = intervalSeconds * 1_000L,
|
|
1983
|
-
)
|
|
1984
|
-
synchronized(streamKeepAliveLock) {
|
|
1985
|
-
activeStreamKeepAlive = tracker
|
|
1986
|
-
}
|
|
1987
|
-
sendNextStreamKeepAlive(tracker)
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
private fun stopStreamKeepAliveMonitor() {
|
|
1991
|
-
val tracker =
|
|
1992
|
-
synchronized(streamKeepAliveLock) {
|
|
1993
|
-
val current = activeStreamKeepAlive
|
|
1994
|
-
activeStreamKeepAlive = null
|
|
1995
|
-
current
|
|
1996
|
-
}
|
|
1997
|
-
tracker?.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
1948
|
private fun handleStreamStatusForRequests(event: StreamStatusEvent) {
|
|
2001
1949
|
val startMatch = matchingStreamStart(event)
|
|
2002
1950
|
if (startMatch != null) {
|
|
@@ -2547,94 +2495,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
2547
2495
|
)
|
|
2548
2496
|
}
|
|
2549
2497
|
|
|
2550
|
-
private fun sendNextStreamKeepAlive(tracker: ActiveStreamKeepAlive) {
|
|
2551
|
-
var timeoutEvent: StreamStatusEvent? = null
|
|
2552
|
-
var request: StreamKeepAliveRequest? = null
|
|
2553
2498
|
|
|
2554
|
-
synchronized(streamKeepAliveLock) {
|
|
2555
|
-
if (activeStreamKeepAlive !== tracker) {
|
|
2556
|
-
return
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
if (tracker.armed && tracker.pendingAckId != null) {
|
|
2560
|
-
tracker.missedAckCount += 1
|
|
2561
|
-
if (tracker.missedAckCount >= MAX_MISSED_STREAM_KEEP_ALIVE_ACKS) {
|
|
2562
|
-
activeStreamKeepAlive = null
|
|
2563
|
-
tracker.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
2564
|
-
timeoutEvent =
|
|
2565
|
-
StreamStatusEvent(
|
|
2566
|
-
StreamStatus.Error(
|
|
2567
|
-
streamId = tracker.streamId,
|
|
2568
|
-
errorDetails =
|
|
2569
|
-
"Stream keep-alive timed out after ${tracker.missedAckCount} missed ACKs",
|
|
2570
|
-
timestamp = System.currentTimeMillis(),
|
|
2571
|
-
resolvedConfig = null,
|
|
2572
|
-
)
|
|
2573
|
-
)
|
|
2574
|
-
return@synchronized
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
val ackId = "ack-${System.currentTimeMillis()}"
|
|
2579
|
-
tracker.pendingAckId = ackId
|
|
2580
|
-
request = StreamKeepAliveRequest(streamId = tracker.streamId, ackId = ackId)
|
|
2581
|
-
val nextTick = Runnable { sendNextStreamKeepAlive(tracker) }
|
|
2582
|
-
tracker.nextTick = nextTick
|
|
2583
|
-
mainHandler.postDelayed(nextTick, tracker.intervalMs)
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
timeoutEvent?.let { event ->
|
|
2587
|
-
dispatchToListeners { it.onStreamStatus(event) }
|
|
2588
|
-
stopStreamKeepAliveMonitor()
|
|
2589
|
-
deviceManager.stopStream()
|
|
2590
|
-
return
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
request?.let { keepAlive ->
|
|
2594
|
-
deviceManager.keepStreamAlive(keepAlive.toMap().toMutableMap())
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
private fun handleStreamKeepAliveAck(event: KeepAliveAckEvent): Boolean {
|
|
2599
|
-
synchronized(streamKeepAliveLock) {
|
|
2600
|
-
val tracker = activeStreamKeepAlive ?: return false
|
|
2601
|
-
if (event.streamId != tracker.streamId || event.ackId != tracker.pendingAckId) {
|
|
2602
|
-
return false
|
|
2603
|
-
}
|
|
2604
|
-
tracker.pendingAckId = null
|
|
2605
|
-
tracker.missedAckCount = 0
|
|
2606
|
-
return true
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
private fun handleStreamStatusForKeepAlive(status: StreamStatus) {
|
|
2611
|
-
val streamId = status.streamId
|
|
2612
|
-
val activeStreamId = synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId }
|
|
2613
|
-
if (streamId == null || activeStreamId != streamId) {
|
|
2614
|
-
return
|
|
2615
|
-
}
|
|
2616
|
-
when (status.state) {
|
|
2617
|
-
StreamState.STOPPED,
|
|
2618
|
-
StreamState.STOPPING,
|
|
2619
|
-
StreamState.ERROR,
|
|
2620
|
-
StreamState.RECONNECT_FAILED -> stopStreamKeepAliveMonitor()
|
|
2621
|
-
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
2622
|
-
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
2623
|
-
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
2624
|
-
// pre-arm bookkeeping so a stale unacked id (sent before the glasses could ACK)
|
|
2625
|
-
// can't immediately count as a miss.
|
|
2626
|
-
else ->
|
|
2627
|
-
synchronized(streamKeepAliveLock) {
|
|
2628
|
-
activeStreamKeepAlive?.let {
|
|
2629
|
-
if (it.streamId == streamId && !it.armed) {
|
|
2630
|
-
it.armed = true
|
|
2631
|
-
it.pendingAckId = null
|
|
2632
|
-
it.missedAckCount = 0
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
2499
|
}
|
|
2639
2500
|
|
|
2640
2501
|
/** OTA status messages are not request-correlated, so only known pre-ack failures settle a start. */
|
|
@@ -4086,6 +4086,14 @@ class MentraLive : SGCManager() {
|
|
|
4086
4086
|
val percent = json.optInt("percent", batteryLevel)
|
|
4087
4087
|
updateBatteryStatus(percent, isCharging)
|
|
4088
4088
|
}
|
|
4089
|
+
"stream_controller_probe" -> {
|
|
4090
|
+
val values = mapOf("protocolVersion" to json.opt("protocolVersion"),
|
|
4091
|
+
"controllerId" to json.opt("controllerId"), "streamId" to json.opt("streamId"),
|
|
4092
|
+
"probeId" to json.opt("probeId"))
|
|
4093
|
+
com.mentra.bluetoothsdk.streaming.StreamControllerProbe.response(values)?.let {
|
|
4094
|
+
sendJson(JSONObject(it))
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4089
4097
|
"pong" ->
|
|
4090
4098
|
// Process heartbeat pong response
|
|
4091
4099
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
@@ -4565,6 +4573,10 @@ class MentraLive : SGCManager() {
|
|
|
4565
4573
|
mapOf("sid" to (glassesSessionId ?: "")),
|
|
4566
4574
|
)
|
|
4567
4575
|
readinessCompletedThisBleSession = true
|
|
4576
|
+
Bridge.sendTypedMessage("stream_control_ready", mapOf(
|
|
4577
|
+
"sid" to json.optString("sid", ""),
|
|
4578
|
+
"streamControlVersion" to json.optInt("streamControlVersion", 0),
|
|
4579
|
+
))
|
|
4568
4580
|
|
|
4569
4581
|
// Set the ready flag to stop any future readiness checks
|
|
4570
4582
|
glassesReady = true
|
|
@@ -6802,6 +6814,8 @@ class MentraLive : SGCManager() {
|
|
|
6802
6814
|
val json = JSONObject(message as Map<*, *>)
|
|
6803
6815
|
// Remove timestamp as iOS does
|
|
6804
6816
|
json.remove("timestamp")
|
|
6817
|
+
json.put("controllerProbeVersion", 1)
|
|
6818
|
+
json.put("controllerId", com.mentra.bluetoothsdk.streaming.StreamControllerProbe.controllerId)
|
|
6805
6819
|
sendJson(json, true)
|
|
6806
6820
|
} catch (e: Exception) {
|
|
6807
6821
|
Log.e(TAG, "Error creating RTMP stream start JSON", e)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.streaming
|
|
2
|
+
|
|
3
|
+
/** Process-scoped native ownership survives BLE reconnects, not app termination. */
|
|
4
|
+
internal object StreamControllerProbe {
|
|
5
|
+
val controllerId: String = java.util.UUID.randomUUID().toString()
|
|
6
|
+
|
|
7
|
+
fun response(values: Map<String, Any?>): Map<String, Any>? {
|
|
8
|
+
if (values["protocolVersion"] != 1 || values["controllerId"] != controllerId) return null
|
|
9
|
+
val streamId = (values["streamId"] as? String)?.takeIf { it.isNotEmpty() } ?: return null
|
|
10
|
+
val probeId = (values["probeId"] as? String)?.takeIf { it.isNotEmpty() } ?: return null
|
|
11
|
+
return mapOf("type" to "stream_controller_response", "protocolVersion" to 1,
|
|
12
|
+
"controllerId" to controllerId, "streamId" to streamId, "probeId" to probeId)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -481,6 +481,14 @@ data class StreamStatusEvent(
|
|
|
481
481
|
val status: StreamStatus,
|
|
482
482
|
val stats: StreamLiveStats? = null,
|
|
483
483
|
) {
|
|
484
|
+
var processSessionId: String? = null
|
|
485
|
+
private set
|
|
486
|
+
var revision: Long? = null
|
|
487
|
+
private set
|
|
488
|
+
var terminal: Boolean? = null
|
|
489
|
+
private set
|
|
490
|
+
var errorDetails: String? = null
|
|
491
|
+
private set
|
|
484
492
|
// True when the glasses will retry the failed publisher themselves
|
|
485
493
|
// (emitting side lands in PR #3488); absent on older firmware and on
|
|
486
494
|
// events not parsed from a glasses status map. Carried here instead of
|
|
@@ -493,6 +501,10 @@ data class StreamStatusEvent(
|
|
|
493
501
|
stats = StreamLiveStats.fromMap(stringMapValue(values["stats"])),
|
|
494
502
|
) {
|
|
495
503
|
willRetry = boolValue(values, "willRetry")
|
|
504
|
+
processSessionId = stringValue(values, "sid")
|
|
505
|
+
revision = longValue(values, "revision")
|
|
506
|
+
terminal = boolValue(values, "terminal")
|
|
507
|
+
errorDetails = stringValue(values, "errorDetails")
|
|
496
508
|
}
|
|
497
509
|
|
|
498
510
|
val state: StreamState get() = status.state
|
|
@@ -501,6 +513,10 @@ data class StreamStatusEvent(
|
|
|
501
513
|
val values: Map<String, Any>
|
|
502
514
|
get() = buildMap {
|
|
503
515
|
putAll(status.toEventMap())
|
|
516
|
+
processSessionId?.let { put("sid", it) }
|
|
517
|
+
revision?.let { put("revision", it) }
|
|
518
|
+
terminal?.let { put("terminal", it) }
|
|
519
|
+
errorDetails?.let { put("errorDetails", it) }
|
|
504
520
|
stats?.let { put("stats", it.toMap()) }
|
|
505
521
|
willRetry?.let { put("willRetry", it) }
|
|
506
522
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.streaming
|
|
2
|
+
|
|
3
|
+
/** Observes glasses-owned streaming; delivery gaps never imply publisher failure. */
|
|
4
|
+
internal class StreamSessionState {
|
|
5
|
+
@Volatile var processSessionId: String? = null
|
|
6
|
+
private set
|
|
7
|
+
@Volatile var currentStreamId: String? = null
|
|
8
|
+
private set
|
|
9
|
+
@Volatile var supported = false
|
|
10
|
+
private set
|
|
11
|
+
private var revision = -1L
|
|
12
|
+
|
|
13
|
+
@Synchronized
|
|
14
|
+
fun ready(sessionId: String?, controlVersion: Int?) {
|
|
15
|
+
supported = controlVersion == 1 && !sessionId.isNullOrEmpty()
|
|
16
|
+
if (processSessionId != sessionId) {
|
|
17
|
+
currentStreamId = null
|
|
18
|
+
revision = -1
|
|
19
|
+
}
|
|
20
|
+
processSessionId = sessionId
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Synchronized
|
|
24
|
+
fun accept(values: Map<String, Any?>): Boolean {
|
|
25
|
+
if (!supported || values["sid"] != processSessionId) return false
|
|
26
|
+
val incomingRevision = (values["revision"] as? Number)?.toLong() ?: return false
|
|
27
|
+
if (incomingRevision < revision) return false
|
|
28
|
+
revision = incomingRevision
|
|
29
|
+
currentStreamId = if (values["terminal"] == true) null else values["streamId"] as? String
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.streaming
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class StreamControllerProbeTest {
|
|
7
|
+
private fun probe() = mapOf<String, Any>("protocolVersion" to 1,
|
|
8
|
+
"controllerId" to StreamControllerProbe.controllerId, "streamId" to "stream", "probeId" to "nonce")
|
|
9
|
+
|
|
10
|
+
@Test fun answersCurrentProcessChallengeWithoutJsOrTimer() {
|
|
11
|
+
val response = StreamControllerProbe.response(probe())!!
|
|
12
|
+
assertEquals("stream_controller_response", response["type"])
|
|
13
|
+
assertEquals("nonce", response["probeId"])
|
|
14
|
+
assertEquals("stream", response["streamId"])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test fun rejectsPreviousProcessAndMalformedChallenges() {
|
|
18
|
+
assertNull(StreamControllerProbe.response(probe() + ("controllerId" to "previous-process")))
|
|
19
|
+
assertNull(StreamControllerProbe.response(probe() + ("protocolVersion" to 2)))
|
|
20
|
+
assertNull(StreamControllerProbe.response(probe() + ("protocolVersion" to true)))
|
|
21
|
+
assertNull(StreamControllerProbe.response(probe() + ("probeId" to "")))
|
|
22
|
+
assertNull(StreamControllerProbe.response(probe() - "streamId"))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.streaming
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class StreamSessionStateTest {
|
|
7
|
+
@Test fun readinessRequiresKnownProtocolAndProcessIdentity() {
|
|
8
|
+
val state = StreamSessionState()
|
|
9
|
+
state.ready("asg", null)
|
|
10
|
+
assertFalse(state.supported)
|
|
11
|
+
state.ready("asg", 2)
|
|
12
|
+
assertFalse(state.supported)
|
|
13
|
+
state.ready("", 1)
|
|
14
|
+
assertFalse(state.supported)
|
|
15
|
+
state.ready("asg", 1)
|
|
16
|
+
assertTrue(state.supported)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test fun reconnectReconcilesTerminalStateWithoutHeartbeatTimeouts() {
|
|
20
|
+
val state = StreamSessionState()
|
|
21
|
+
state.ready("asg", 1)
|
|
22
|
+
assertTrue(state.accept(event("asg", 1, false)))
|
|
23
|
+
state.ready("asg", 1)
|
|
24
|
+
assertEquals("stream", state.currentStreamId)
|
|
25
|
+
assertTrue(state.accept(event("asg", 3, true)))
|
|
26
|
+
assertNull(state.currentStreamId)
|
|
27
|
+
assertFalse(state.accept(event("asg", 2, false)))
|
|
28
|
+
assertNull(state.currentStreamId)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test fun restartRejectsOldStatusAndAcceptsNewStoppedSnapshot() {
|
|
32
|
+
val state = StreamSessionState()
|
|
33
|
+
state.ready("old", 1)
|
|
34
|
+
assertTrue(state.accept(event("old", 8, false)))
|
|
35
|
+
state.ready("new", 1)
|
|
36
|
+
assertFalse(state.accept(event("old", 9, false)))
|
|
37
|
+
assertTrue(state.accept(mapOf("sid" to "new", "revision" to 0, "terminal" to true)))
|
|
38
|
+
assertNull(state.currentStreamId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun event(sid: String, revision: Int, terminal: Boolean) =
|
|
42
|
+
mapOf("sid" to sid, "revision" to revision, "streamId" to "stream", "terminal" to terminal)
|
|
43
|
+
}
|
|
@@ -719,6 +719,11 @@ export type StreamLiveStats = {
|
|
|
719
719
|
type StreamStatusCommon = {
|
|
720
720
|
type: "stream_status";
|
|
721
721
|
streamId?: string;
|
|
722
|
+
/** ASG process identity and monotonic revision for reconnect reconciliation. */
|
|
723
|
+
sid?: string;
|
|
724
|
+
revision?: number;
|
|
725
|
+
terminal?: boolean;
|
|
726
|
+
errorDetails?: string;
|
|
722
727
|
timestamp?: number;
|
|
723
728
|
resolvedConfig?: StreamResolvedConfig;
|
|
724
729
|
stats?: StreamLiveStats;
|
|
@@ -746,7 +751,7 @@ export type StreamStatusEvent = (StreamStatusCommon & {
|
|
|
746
751
|
errorDetails: string;
|
|
747
752
|
}) | (StreamStatusCommon & {
|
|
748
753
|
kind: "snapshot";
|
|
749
|
-
status:
|
|
754
|
+
status: StreamStatusState;
|
|
750
755
|
streaming: boolean;
|
|
751
756
|
reconnecting: boolean;
|
|
752
757
|
attempt?: number;
|