Skip to content

Commit ad0b066

Browse files
authored
Merge pull request #2124 from Mentra-Community/fix-mentraai-snap
mentra live can disable shutter sound for mentra ai, keep flash
2 parents 838a14b + 2a9547e commit ad0b066

32 files changed

Lines changed: 352 additions & 505 deletions

asg_client/app/src/main/java/com/mentra/asg_client/io/media/core/MediaCaptureService.java

Lines changed: 64 additions & 48 deletions
Large diffs are not rendered by default.

asg_client/app/src/main/java/com/mentra/asg_client/io/streaming/services/RtmpStreamingService.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ private enum StreamState {
130130
// Reconnection sequence tracking to prevent stale handlers
131131
private int mReconnectionSequence = 0;
132132

133-
// LED control
133+
// LED and sound control
134134
private IHardwareManager mHardwareManager;
135135
private boolean mLedEnabled = false;
136+
private boolean mSoundEnabled = false;
136137

137138
// Battery monitoring for RTMP streaming
138139
private IStateManager mStateManager;
@@ -199,6 +200,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
199200
String rtmpUrl = intent.getStringExtra("rtmp_url");
200201
String streamId = intent.getStringExtra("stream_id");
201202
mLedEnabled = intent.getBooleanExtra("enable_led", true); // Default true for livestreams
203+
mSoundEnabled = intent.getBooleanExtra("enable_sound", true); // Default true for livestreams
202204

203205
if (rtmpUrl != null && !rtmpUrl.isEmpty()) {
204206
setRtmpUrl(rtmpUrl);
@@ -473,6 +475,12 @@ public void onSuccess() {
473475
Log.d(TAG, "📹 Recording LED turned ON for livestream");
474476
}
475477

478+
// Play stream start sound
479+
if (mSoundEnabled && mHardwareManager != null && mHardwareManager.supportsAudioPlayback()) {
480+
mHardwareManager.playAudioAsset(AudioAssets.VIDEO_RECORDING_START);
481+
Log.d(TAG, "🔊 Stream start sound played");
482+
}
483+
476484
// Start battery monitoring
477485
startBatteryMonitoring();
478486

@@ -1089,6 +1097,12 @@ public void resumeWith(Object o) {
10891097
}
10901098
}
10911099

1100+
// Play stream stop sound (only on actual stop, not reconnection)
1101+
if (!preserveSession && mSoundEnabled && mHardwareManager != null && mHardwareManager.supportsAudioPlayback()) {
1102+
mHardwareManager.playAudioAsset(AudioAssets.VIDEO_RECORDING_STOP);
1103+
Log.d(TAG, "🔊 Stream stop sound played");
1104+
}
1105+
10921106
if (preserveSession) {
10931107
Log.d(TAG, "Stream resources released for reconnection");
10941108
} else {
@@ -1396,9 +1410,10 @@ public static void setStreamConfig(RtmpStreamConfig config) {
13961410
* @param rtmpUrl RTMP URL to stream to
13971411
* @param streamId Stream ID for tracking (can be null)
13981412
* @param enableLed Whether to enable recording LED during stream
1413+
* @param enableSound Whether to enable start/stop sounds during stream
13991414
* @param config Stream configuration (video/audio settings). Pass null for defaults.
14001415
*/
1401-
public static void startStreaming(Context context, String rtmpUrl, String streamId, boolean enableLed, RtmpStreamConfig config) {
1416+
public static void startStreaming(Context context, String rtmpUrl, String streamId, boolean enableLed, boolean enableSound, RtmpStreamConfig config) {
14021417
// Set config first (before service starts or before streaming begins)
14031418
setStreamConfig(config);
14041419

@@ -1416,28 +1431,31 @@ public static void startStreaming(Context context, String rtmpUrl, String stream
14161431
sInstance.setRtmpUrl(rtmpUrl);
14171432
sInstance.mCurrentStreamId = streamId; // Set the stream ID
14181433
sInstance.mLedEnabled = enableLed; // Set LED state
1434+
sInstance.mSoundEnabled = enableSound; // Set sound state
14191435
sInstance.startStreaming();
14201436
} else {
1421-
// Start the service with the provided URL, stream ID, and LED setting
1437+
// Start the service with the provided URL, stream ID, and LED/sound settings
14221438
Intent intent = new Intent(context, RtmpStreamingService.class);
14231439
intent.putExtra("rtmp_url", rtmpUrl);
14241440
if (streamId != null && !streamId.isEmpty()) {
14251441
intent.putExtra("stream_id", streamId);
14261442
}
14271443
intent.putExtra("enable_led", enableLed);
1444+
intent.putExtra("enable_sound", enableSound);
14281445
context.startService(intent);
14291446
}
14301447
}
14311448

14321449
/**
1433-
* Start streaming to the specified RTMP URL with LED control
1450+
* Start streaming to the specified RTMP URL with LED and sound control
14341451
* @param context Context to use for starting the service
14351452
* @param rtmpUrl RTMP URL to stream to
14361453
* @param streamId Stream ID for tracking (can be null)
14371454
* @param enableLed Whether to enable recording LED during stream
1455+
* @param enableSound Whether to enable start/stop sounds during stream
14381456
*/
1439-
public static void startStreaming(Context context, String rtmpUrl, String streamId, boolean enableLed) {
1440-
startStreaming(context, rtmpUrl, streamId, enableLed, null); // Use default config
1457+
public static void startStreaming(Context context, String rtmpUrl, String streamId, boolean enableLed, boolean enableSound) {
1458+
startStreaming(context, rtmpUrl, streamId, enableLed, enableSound, null); // Use default config
14411459
}
14421460

14431461
/**
@@ -1447,7 +1465,7 @@ public static void startStreaming(Context context, String rtmpUrl, String stream
14471465
* @param streamId Stream ID for tracking (can be null)
14481466
*/
14491467
public static void startStreaming(Context context, String rtmpUrl, String streamId) {
1450-
startStreaming(context, rtmpUrl, streamId, true); // Default LED on
1468+
startStreaming(context, rtmpUrl, streamId, true, true); // Default LED and sound on
14511469
}
14521470

14531471
/**
@@ -1456,7 +1474,7 @@ public static void startStreaming(Context context, String rtmpUrl, String stream
14561474
* @param rtmpUrl RTMP URL to stream to
14571475
*/
14581476
public static void startStreaming(Context context, String rtmpUrl) {
1459-
startStreaming(context, rtmpUrl, null, true); // Default LED on
1477+
startStreaming(context, rtmpUrl, null, true, true); // Default LED and sound on
14601478
}
14611479

14621480
/**

asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/K900CommandHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ private void handlePhotoCapture(boolean isLongPress) {
732732
Log.d(TAG, "📸 Taking photo locally (short press) with LED: " + ledEnabled);
733733
// Get saved photo size for button press
734734
String photoSize = serviceManager.getAsgSettings().getButtonPhotoSize();
735-
captureService.takePhotoLocally(photoSize, ledEnabled);
735+
captureService.takePhotoLocally(photoSize, ledEnabled, true);
736736
}
737737
}
738738
}

asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/PhotoCommandHandler.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ private boolean handleTakePhoto(JSONObject data) {
7474
boolean save = data.optBoolean("save", false);
7575
String size = data.optString("size", "medium");
7676
String compress = data.optString("compress", "none"); // Default to none (no compression)
77-
// silent: true = no sound/LED, false (default) = normal behavior with sound/LED
78-
boolean silent = data.optBoolean("silent", false);
79-
boolean enableLed = !silent; // Convert to internal enableLed (inverted logic)
77+
boolean flash = data.optBoolean("flash", true);
78+
boolean sound = data.optBoolean("sound", true);
8079

8180
// Generate file path using base class functionality
8281
String fileName = generateUniqueFilename("IMG_", ".jpg");
@@ -133,7 +132,7 @@ private boolean handleTakePhoto(JSONObject data) {
133132

134133
// Process photo capture based on transfer method
135134
boolean success = processPhotoCapture(captureService, photoFilePath, requestId, webhookUrl, authToken,
136-
bleImgId, save, size, transferMethod, enableLed, compress);
135+
bleImgId, save, size, transferMethod, flash, sound, compress);
137136
logCommandResult("take_photo", success, success ? null : "Photo capture failed");
138137
return success;
139138

@@ -156,27 +155,28 @@ private boolean handleTakePhoto(JSONObject data) {
156155
* @param save Whether to save the photo
157156
* @param size Photo size
158157
* @param transferMethod Transfer method
159-
* @param enableLed Whether to enable LED
158+
* @param flash Whether to enable privacy flash LED
159+
* @param sound Whether to enable shutter sound
160160
* @param compress Compression level
161161
* @return true if successful, false otherwise
162162
*/
163163
private boolean processPhotoCapture(MediaCaptureService captureService, String photoFilePath,
164164
String requestId, String webhookUrl, String authToken, String bleImgId,
165-
boolean save, String size, String transferMethod, boolean enableLed, String compress) {
165+
boolean save, String size, String transferMethod, boolean flash, boolean sound, String compress) {
166166
Log.d(TAG, "789789Processing photo capture with transfer method: " + transferMethod);
167167
switch (transferMethod) {
168168
case "ble":
169-
captureService.takePhotoForBleTransfer(photoFilePath, requestId, bleImgId, save, size, enableLed);
169+
captureService.takePhotoForBleTransfer(photoFilePath, requestId, bleImgId, save, size, flash, sound);
170170
return true;
171171
case "auto":
172172
if (bleImgId.isEmpty()) {
173173
Log.e(TAG, "Auto mode requires bleImgId for fallback");
174174
return false;
175175
}
176-
captureService.takePhotoAutoTransfer(photoFilePath, requestId, webhookUrl, authToken, bleImgId, save, size, enableLed, compress);
176+
captureService.takePhotoAutoTransfer(photoFilePath, requestId, webhookUrl, authToken, bleImgId, save, size, flash, sound, compress);
177177
return true;
178178
default:
179-
captureService.takePhotoAndUpload(photoFilePath, requestId, webhookUrl, authToken, save, size, enableLed, compress);
179+
captureService.takePhotoAndUpload(photoFilePath, requestId, webhookUrl, authToken, save, size, flash, sound, compress);
180180
return true;
181181
}
182182
}

asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/RtmpCommandHandler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,8 @@ private boolean handleStartRtmpStream(JSONObject data) {
109109
}
110110

111111
String streamId = data.optString("streamId", "");
112-
// silent: true = no sound/LED, false (default) = normal behavior with sound/LED
113-
boolean silent = data.optBoolean("silent", false);
114-
boolean enableLed = !silent; // Convert to internal enableLed (inverted logic)
112+
boolean flash = data.optBoolean("flash", true);
113+
boolean sound = data.optBoolean("sound", true);
115114

116115
// Parse video/audio config from SDK message (supports both full and compact keys)
117116
// Full: { video: {...}, audio: {...} }
@@ -127,7 +126,7 @@ private boolean handleStartRtmpStream(JSONObject data) {
127126
RtmpStreamConfig streamConfig = RtmpStreamConfig.fromJson(videoJson, audioJson);
128127

129128
Log.d(TAG, "Starting RTMP stream with config: " + streamConfig.toString());
130-
RtmpStreamingService.startStreaming(context, rtmpUrl, streamId, enableLed, streamConfig);
129+
RtmpStreamingService.startStreaming(context, rtmpUrl, streamId, flash, sound, streamConfig);
131130

132131
// Set StateManager for battery monitoring
133132
RtmpStreamingService.setStateManager(stateManager);

asg_client/app/src/main/java/com/mentra/asg_client/service/core/handlers/VideoCommandHandler.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,14 @@ private boolean handleStartVideoRecording(JSONObject data) {
139139

140140
// Start recording with settings
141141
boolean save = data.optBoolean("save", false);
142-
// silent: true = no sound/LED, false (default) = normal behavior with sound/LED
143-
boolean silent = data.optBoolean("silent", false);
144-
boolean enableLed = !silent; // Convert to internal enableLed (inverted logic)
142+
boolean flash = data.optBoolean("flash", true);
143+
boolean sound = data.optBoolean("sound", true);
145144
String requestId = data.optString("requestId", "video_" + System.currentTimeMillis());
146-
145+
147146
if (videoSettings != null) {
148-
captureService.handleStartVideoCommand(requestId, save, videoSettings, enableLed);
147+
captureService.handleStartVideoCommand(requestId, save, videoSettings, flash, sound);
149148
} else {
150-
captureService.handleStartVideoCommand(requestId, save, enableLed); // Use default settings
149+
captureService.handleStartVideoCommand(requestId, save, flash, sound); // Use default settings
151150
}
152151

153152
logCommandResult("start_video_recording", true, null);

cloud/packages/cloud/src/services/session/PhotoManager.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,6 @@ import { ConnectionValidator } from "../validators/ConnectionValidator";
1717

1818
// Timeout handling is managed by CameraModule in the SDK
1919

20-
/**
21-
* Packages that should have silent photo mode (no LED flash, no shutter sound).
22-
* These are AI apps that take photos continuously for context awareness.
23-
*/
24-
const SILENT_PHOTO_PACKAGES_HARDCODED = ["com.mentra.mira", "com.mentra.mentraai", "com.mentra.mentraai.beta"];
25-
26-
// Build the allowlist: hardcoded + env var
27-
const envPackages =
28-
process.env.SILENT_PHOTO_PACKAGES?.split(",")
29-
.map((p) => p.trim())
30-
.filter(Boolean) || [];
31-
const SILENT_PHOTO_PACKAGES = new Set([...SILENT_PHOTO_PACKAGES_HARDCODED, ...envPackages]);
32-
3320
/**
3421
* Internal representation of a pending photo request,
3522
* adapted from PendingPhotoRequest in photo-request.service.ts.
@@ -133,8 +120,9 @@ export class PhotoManager {
133120
};
134121
this.pendingPhotoRequests.set(requestId, requestInfo);
135122

136-
// Determine if this app should use silent mode (no LED flash, no shutter sound)
137-
const silent = SILENT_PHOTO_PACKAGES.has(packageName);
123+
// Flash is always on (privacy indicator for bystanders), sound is app-controlled via SDK
124+
const flash = true;
125+
const sound = appRequest.sound ?? true;
138126

139127
// Message to glasses based on CloudToGlassesMessageType.PHOTO_REQUEST
140128
// Include webhook URL so ASG can upload directly to the app
@@ -147,7 +135,8 @@ export class PhotoManager {
147135
authToken, // Include authToken for webhook authentication
148136
size, // Propagate desired size
149137
compress, // Propagate compression setting
150-
silent, // Silent mode: disables LED flash and shutter sound for AI apps
138+
flash, // Controls privacy flash LED (cloud-controlled)
139+
sound, // Controls shutter sound (app-controllable via SDK)
151140
timestamp: new Date(),
152141
};
153142

@@ -160,9 +149,10 @@ export class PhotoManager {
160149
webhookUrl,
161150
isCustom: !!customWebhookUrl,
162151
hasAuthToken: !!authToken,
163-
silent,
152+
flash,
153+
sound,
164154
},
165-
`PHOTO_REQUEST command sent to glasses${silent ? " (silent mode)" : ""}.`,
155+
`PHOTO_REQUEST command sent to glasses (flash=${flash}, sound=${sound}).`,
166156
);
167157

168158
// If using custom webhook URL, resolve immediately since glasses won't send response back to cloud

0 commit comments

Comments
 (0)