Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recall pure socket #883

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
12 changes: 11 additions & 1 deletion app/lib/backend/schema/bt_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import 'package:friend_private/services/device_connections.dart';
import 'package:friend_private/services/frame_connection.dart';
import 'package:friend_private/utils/ble/gatt_utils.dart';

enum BleAudioCodec { pcm16, pcm8, mulaw16, mulaw8, opus, unknown }
enum BleAudioCodec {
pcm16,
pcm8,
mulaw16,
mulaw8,
opus,
unknown;

@override
String toString() => mapCodecToName(this);
}

String mapCodecToName(BleAudioCodec codec) {
switch (codec) {
Expand Down
4 changes: 2 additions & 2 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,12 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
ChangeNotifierProxyProvider3<MemoryProvider, MessageProvider, WebSocketProvider, CaptureProvider>(
create: (context) => CaptureProvider(),
update: (BuildContext context, memory, message, wsProvider, CaptureProvider? previous) =>
(previous?..updateProviderInstances(memory, message, wsProvider)) ?? CaptureProvider(),
(previous?..updateProviderInstances(memory, message)) ?? CaptureProvider(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The wsProvider argument has been removed from the updateProviderInstances method call. This could potentially cause issues if the CaptureProvider class's updateProviderInstances method still expects three arguments. Please ensure that the method signature for updateProviderInstances in the CaptureProvider class has been updated to match this change.

- (previous?..updateProviderInstances(memory, message, wsProvider)) ?? CaptureProvider(),
+ (previous?..updateProviderInstances(memory, message)) ?? CaptureProvider(),

),
ChangeNotifierProxyProvider2<CaptureProvider, WebSocketProvider, DeviceProvider>(
create: (context) => DeviceProvider(),
update: (BuildContext context, captureProvider, wsProvider, DeviceProvider? previous) =>
(previous?..setProviders(captureProvider, wsProvider)) ?? DeviceProvider(),
(previous?..setProviders(captureProvider)) ?? DeviceProvider(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The ChangeNotifierProxyProvider3 and ChangeNotifierProxyProvider2 functions have been modified to remove the wsProvider parameter from their update methods. This change could potentially lead to issues if the wsProvider was being used in the updateProviderInstances or setProviders methods. Please ensure that these methods do not require the wsProvider for their functionality, or refactor them accordingly.

- (previous?..updateProviderInstances(memory, message, wsProvider)) ?? CaptureProvider(),
+ (previous?..updateProviderInstances(memory, message)) ?? CaptureProvider(),

- (previous?..setProviders(captureProvider, wsProvider)) ?? DeviceProvider(),
+ (previous?..setProviders(captureProvider)) ?? DeviceProvider(),

),
ChangeNotifierProxyProvider<DeviceProvider, OnboardingProvider>(
create: (context) => OnboardingProvider(),
Expand Down
265 changes: 3 additions & 262 deletions app/lib/pages/capture/_page.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_provider_utilities/flutter_provider_utilities.dart';
import 'package:friend_private/backend/schema/bt_device.dart';
import 'package:friend_private/backend/schema/geolocation.dart';
import 'package:friend_private/pages/capture/widgets/widgets.dart';
import 'package:friend_private/providers/capture_provider.dart';
import 'package:friend_private/providers/connectivity_provider.dart';
import 'package:friend_private/providers/device_provider.dart';
import 'package:friend_private/providers/onboarding_provider.dart';
import 'package:friend_private/utils/audio/wav_bytes.dart';
import 'package:friend_private/utils/ble/communication.dart';
import 'package:friend_private/utils/enums.dart';
import 'package:friend_private/widgets/dialog.dart';
import 'package:provider/provider.dart';

import '../../providers/websocket_provider.dart';

@Deprecated("Capture page is deprecated, use @pages > memories > widgets > capture instead.")
class CapturePage extends StatefulWidget {
const CapturePage({
super.key,
Comment on lines 1 to 6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

Note: This review was outside of the patch, so it was mapped to the patch with the greatest overlap. Original lines [3-7]

The CapturePage class has been deprecated and replaced with a simple text widget displaying "Deprecated". This is a significant change as it removes a lot of functionality previously present in the CapturePage. Ensure that all the functionalities have been moved to the new location before deprecating this page.

- @Deprecated("Capture page is deprecated, use @pages > memories > widgets > capture instead.")
- class CapturePage extends StatefulWidget {
-   const CapturePage({
-     super.key,
+ class CapturePage extends StatelessWidget {
+   @override
+   Widget build(BuildContext context) {
+     return const Text("This page has been deprecated. Please use @pages > memories > widgets > capture instead.");
+   }

Expand All @@ -26,252 +10,9 @@ class CapturePage extends StatefulWidget {
State<CapturePage> createState() => CapturePageState();
}

class CapturePageState extends State<CapturePage> with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
@override
bool get wantKeepAlive => true;

/// ----

// List<TranscriptSegment> segments = List.filled(100, '')
// .mapIndexed((i, e) => TranscriptSegment(
// text:
// '''[00:00:00 - 00:02:23] Speaker 0: The tech giants already know these techniques.
// My goal is to unlock their secrets for the benefit of businesses who to design and help users develop healthy habits.
// To that end, there's so much I wanted to put in this book that just didn't fit. Before you reading, please take a moment to download these
// supplementary materials included free with the purchase of this audiobook. Please go to nirandfar.com forward slash hooked.
// Near is spelled like my first name, speck, n I r. Andfar.com/hooked. There you will find the hooked model workbook, an ebook of case studies,
// and a free email course about product psychology. Also, if you'd like to connect with me, you can reach me through my blog at nirafar.com.
// You can schedule office hours to discuss your questions. Look forward to hearing from you as you build habits for good.
//
// Introduction. 79% of smartphone owners check their device within 15 minutes of waking up every morning. Perhaps most startling,
// fully 1 third of Americans say they would rather give up sex than lose their cell phones. A 2011 university study suggested people check their
// phones 34 times per day. However, industry insiders believe that number is closer to an astounding 150 daily sessions. We are hooked.
// It's the poll to visit YouTube, Facebook, or Twitter for just a few minutes only to find yourself still capping and scrolling an hour later.
// It's the urge you likely feel throughout your day but hardly notice. Cognitive psychologists define habits as, quote, automatic behaviors triggered
// by situational cues. Things we do with little or no conscious thought. The products and services we use habitually alter our everyday behavior.
// Just as their designers intended. Our actions have been engineered. How do companies producing little more than bits of code displayed on a screen
// seemingly control users' minds? What makes some products so habit forming? Forming habit is imperative for the survival of many products.
//
// As infinite distractions compete for our attention, companies are learning to master novel tactics that stay relevant in users' minds.
// Amassing millions of users is no longer good enough. Companies increasingly find that their economic value is a function of the strength of the habits they create.
//
// In order to win the loyalty of their users and create a product that's regularly used, companies must learn not only what compels users to click,
// but also what makes them click. Although some companies are just waking up to this new reality, others are already cashing in. By mastering habit
// forming product design, companies profiles in this book make their goods indispensable. First to mind wins. Companies that form strong user habits enjoy
// several benefits to their bottom line. These companies attach their product to internal triggers. A result, users show up without any external prompting.
// Instead of relying on expensive marketing, how did forming companies link their services to users' daily routines and emotions.
// A habit is at work when users feel a tad bored and instantly open Twitter. Feel a hang of loneliness, and before rational thought occurs,
// they're scrolling through their Facebook feeds.''',
// speaker: 'SPEAKER_0${i % 2}',
// isUser: false,
// start: 0,
// end: 10,
// ))
// .toList();

setHasTranscripts(bool hasTranscripts) {
context.read<CaptureProvider>().setHasTranscripts(hasTranscripts);
}

void _onReceiveTaskData(dynamic data) {
if (data is Map<String, dynamic>) {
if (data.containsKey('latitude') && data.containsKey('longitude')) {
context.read<CaptureProvider>().setGeolocation(Geolocation(
latitude: data['latitude'],
longitude: data['longitude'],
accuracy: data['accuracy'],
altitude: data['altitude'],
time: DateTime.parse(data['time']),
));
} else {
if (mounted) {
context.read<CaptureProvider>().setGeolocation(null);
}
}
}
}

@override
void initState() {
WavBytesUtil.clearTempWavFiles();

FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
WidgetsBinding.instance.addObserver(this);
SchedulerBinding.instance.addPostFrameCallback((_) async {
// await context.read<CaptureProvider>().processCachedTranscript();
if (context.read<DeviceProvider>().connectedDevice != null) {
context.read<OnboardingProvider>().stopFindDeviceTimer();
}
// if (await LocationService().displayPermissionsDialog()) {
// await showDialog(
// context: context,
// builder: (c) => getDialog(
// context,
// () => Navigator.of(context).pop(),
// () async {
// await requestLocationPermission();
// await LocationService().requestBackgroundPermission();
// if (mounted) Navigator.of(context).pop();
// },
// 'Enable Location? 🌍',
// 'Allow location access to tag your memories. Set to "Always Allow" in Settings',
// singleButton: false,
// okButtonText: 'Continue',
// ),
// );
// }
final connectivityProvider = Provider.of<ConnectivityProvider>(context, listen: false);
if (!connectivityProvider.isConnected) {
context.read<CaptureProvider>().cancelMemoryCreationTimer();
}
});

super.initState();
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
// context.read<WebSocketProvider>().closeWebSocket();
super.dispose();
}

// Future requestLocationPermission() async {
// LocationService locationService = LocationService();
// bool serviceEnabled = await locationService.enableService();
// if (!serviceEnabled) {
// debugPrint('Location service not enabled');
// if (mounted) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text(
// 'Location services are disabled. Enable them for a better experience.',
// style: TextStyle(color: Colors.white, fontSize: 14),
// ),
// ),
// );
// }
// } else {
// PermissionStatus permissionGranted = await locationService.requestPermission();
// SharedPreferencesUtil().locationEnabled = permissionGranted == PermissionStatus.granted;
// MixpanelManager().setUserProperty('Location Enabled', SharedPreferencesUtil().locationEnabled);
// if (permissionGranted == PermissionStatus.denied) {
// debugPrint('Location permission not granted');
// } else if (permissionGranted == PermissionStatus.deniedForever) {
// debugPrint('Location permission denied forever');
// if (mounted) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text(
// 'If you change your mind, you can enable location services in your device settings.',
// style: TextStyle(color: Colors.white, fontSize: 14),
// ),
// ),
// );
// }
// }
// }
// }

class CapturePageState extends State<CapturePage> {
@override
Widget build(BuildContext context) {
super.build(context);
return Consumer2<CaptureProvider, DeviceProvider>(builder: (context, provider, deviceProvider, child) {
return MessageListener<CaptureProvider>(
showInfo: (info) {
// This probably will never be called because this has been handled even before we start the audio stream. But it's here just in case.
if (info == 'FIM_CHANGE') {
showDialog(
context: context,
barrierDismissible: false,
builder: (c) => getDialog(
context,
() async {
context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Firmware change detected');
var connectedDevice = deviceProvider.connectedDevice;
var codec = await getAudioCodec(connectedDevice!.id);
context.read<CaptureProvider>().resetState(restartBytesProcessing: true);
context.read<CaptureProvider>().initiateWebsocket(codec);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
() => {},
'Firmware change detected!',
'You are currently using a different firmware version than the one you were using before. Please restart the app to apply the changes.',
singleButton: true,
okButtonText: 'Restart',
),
);
}
},
showError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
error,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
);
},
child: Stack(
children: [
ListView(children: [
SpeechProfileCardWidget(),
...getConnectionStateWidgets(
context,
provider.hasTranscripts,
deviceProvider.connectedDevice,
context.read<WebSocketProvider>().wsConnectionState,
),
getTranscriptWidget(
provider.memoryCreating,
provider.segments,
provider.photos,
deviceProvider.connectedDevice,
),
...connectionStatusWidgets(
context,
provider.segments,
context.read<WebSocketProvider>().wsConnectionState,
),
const SizedBox(height: 16)
]),
getPhoneMicRecordingButton(() => _recordingToggled(provider), provider.recordingState),
],
),
);
});
}

_recordingToggled(CaptureProvider provider) async {
var recordingState = provider.recordingState;
if (recordingState == RecordingState.record) {
provider.stopStreamRecording();
provider.updateRecordingState(RecordingState.stop);
context.read<CaptureProvider>().cancelMemoryCreationTimer();
// await context.read<CaptureProvider>().tryCreateMemoryManually();
} else if (recordingState == RecordingState.initialising) {
debugPrint('initialising, have to wait');
} else {
showDialog(
context: context,
builder: (c) => getDialog(
context,
() => Navigator.pop(context),
() async {
provider.updateRecordingState(RecordingState.initialising);
context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
await provider.initiateWebsocket(BleAudioCodec.pcm16, 16000);
await provider.streamRecording();
Navigator.pop(context);
},
'Limited Capabilities',
'Recording with your phone microphone has a few limitations, including but not limited to: speaker profiles, background reliability.',
okButtonText: 'Ok, I understand',
),
);
}
return const Text("Depreacted");
Comment on lines +13 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The CapturePageState class has also been significantly simplified. It now only contains a build method that returns a Text widget. If there was any state management or lifecycle methods in the previous implementation, ensure they are properly handled in the new structure.

- class CapturePageState extends State<CapturePage> {
-    @override
-    Widget build(BuildContext context) {
-     return const Text("Depreacted");
-   }
+ class CapturePageState extends State<CapturePage> {
+    // Add necessary state management and lifecycle methods here
+    @override
+    Widget build(BuildContext context) {
+     return const Text("This page has been deprecated. Please use @pages > memories > widgets > capture instead.");
+   }

}
}
3 changes: 2 additions & 1 deletion app/lib/pages/capture/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ class SpeechProfileCardWidget extends StatelessWidget {
if (hasSpeakerProfile != SharedPreferencesUtil().hasSpeakerProfile) {
if (context.mounted) {
// TODO: is the websocket restarting once the user comes back?
context.read<DeviceProvider>().restartWebSocket();
// TODO: thinh, socket speech profile
// context.read<DeviceProvider>().restartWebSocket();
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion app/lib/pages/home/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver, Ticker
if (language != SharedPreferencesUtil().recordingsLanguage ||
hasSpeech != SharedPreferencesUtil().hasSpeakerProfile ||
transcriptModel != SharedPreferencesUtil().transcriptionModel) {
context.read<DeviceProvider>().restartWebSocket();
// TODO: thinh, socket speech profile
// context.read<DeviceProvider>().restartWebSocket();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The WebSocket restart logic has been commented out. If this is intentional and the functionality is being handled elsewhere, consider removing these lines entirely to avoid confusion. If the functionality is not handled elsewhere, this could lead to issues with the WebSocket connection not being properly restarted when necessary.

- // TODO: thinh, socket speech profile
- // context.read<DeviceProvider>().restartWebSocket();

}
},
),
Expand Down
19 changes: 10 additions & 9 deletions app/lib/pages/memories/widgets/capture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ class LiteCaptureWidgetState extends State<LiteCaptureWidget>
void _onReceiveTaskData(dynamic data) {
if (data is Map<String, dynamic>) {
if (data.containsKey('latitude') && data.containsKey('longitude')) {
context.read<CaptureProvider>().setGeolocation(Geolocation(
latitude: data['latitude'],
longitude: data['longitude'],
accuracy: data['accuracy'],
altitude: data['altitude'],
time: DateTime.parse(data['time']),
));
if (mounted) {
context.read<CaptureProvider>().setGeolocation(Geolocation(
latitude: data['latitude'],
longitude: data['longitude'],
accuracy: data['accuracy'],
altitude: data['altitude'],
time: DateTime.parse(data['time']),
));
}
Comment on lines +37 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The check for mounted before setting the geolocation is a good practice to prevent calling setState when the widget is no longer in the widget tree. This can help avoid unnecessary exceptions.

-        context.read<CaptureProvider>().setGeolocation(Geolocation(
-              latitude: data['latitude'],
-              longitude: data['longitude'],
-              accuracy: data['accuracy'],
-              altitude: data['altitude'],
-              time: DateTime.parse(data['time']),
-            ));
+        if (mounted) {
+          context.read<CaptureProvider>().setGeolocation(Geolocation(
+                latitude: data['latitude'],
+                longitude: data['longitude'],
+                accuracy: data['accuracy'],
+                altitude: data['altitude'],
+                time: DateTime.parse(data['time']),
+              ));
+        }

} else {
if (mounted) {
context.read<CaptureProvider>().setGeolocation(null);
Expand Down Expand Up @@ -139,8 +141,7 @@ class LiteCaptureWidgetState extends State<LiteCaptureWidget>
context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Firmware change detected');
var connectedDevice = deviceProvider.connectedDevice;
var codec = await _getAudioCodec(connectedDevice!.id);
context.read<CaptureProvider>().resetState(restartBytesProcessing: true);
context.read<CaptureProvider>().initiateWebsocket(codec);
await context.read<CaptureProvider>().changeAudioRecordProfile(codec);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The refactoring of resetting state and initiating the websocket into a single method changeAudioRecordProfile improves code readability and maintainability by encapsulating related operations into one function.

-                  context.read<CaptureProvider>().resetState(restartBytesProcessing: true);
-                  context.read<CaptureProvider>().initiateWebsocket(codec);
+                  await context.read<CaptureProvider>().changeAudioRecordProfile(codec);

Comment on lines 140 to +142

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The new code seems to be missing the functionality of closing the WebSocket without reconnecting when a firmware change is detected. This was present in the old code but has been removed in the new one. If this functionality is still required, it should be reintroduced.

+ context.read<SocketServicePool>().closeSocketWithoutReconnect('Firmware change detected');
  var connectedDevice = deviceProvider.connectedDevice;
  var codec = await _getAudioCodec(connectedDevice!.id);
  await context.read<CaptureProvider>().changeAudioRecordProfile(codec);

Please ensure that SocketServicePool has a method closeSocketWithoutReconnect similar to the old WebSocketProvider. If not, you may need to implement it.

if (Navigator.canPop(context)) {
Navigator.pop(context);
}
Expand Down
8 changes: 4 additions & 4 deletions app/lib/pages/memories/widgets/processing_capture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:friend_private/pages/memory_capturing/page.dart';
import 'package:friend_private/providers/capture_provider.dart';
import 'package:friend_private/providers/connectivity_provider.dart';
import 'package:friend_private/providers/device_provider.dart';
import 'package:friend_private/providers/websocket_provider.dart';
import 'package:friend_private/utils/analytics/mixpanel.dart';
import 'package:friend_private/utils/enums.dart';
import 'package:friend_private/utils/other/temp.dart';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The import statement for websocket_provider.dart has been removed, which is consistent with the PR's goal of removing the dependency on WebSocketProvider. Ensure that all references to this provider have been properly refactored or removed in the rest of the codebase.

- import 'package:friend_private/providers/websocket_provider.dart';

Expand Down Expand Up @@ -80,7 +79,7 @@ class _MemoryCaptureWidgetState extends State<MemoryCaptureWidget> {
_toggleRecording(BuildContext context, CaptureProvider provider) async {
var recordingState = provider.recordingState;
if (recordingState == RecordingState.record) {
provider.stopStreamRecording();
await provider.stopStreamRecording();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The stopStreamRecording method call has been updated to be awaited. This is a good practice as it ensures that the recording is fully stopped before proceeding to the next line of execution.

- provider.stopStreamRecording();
+ await provider.stopStreamRecording();

context.read<CaptureProvider>().cancelMemoryCreationTimer();
await context.read<CaptureProvider>().createMemory();
MixpanelManager().phoneMicRecordingStopped();
Expand All @@ -95,8 +94,9 @@ class _MemoryCaptureWidgetState extends State<MemoryCaptureWidget> {
() async {
Navigator.pop(context);
provider.updateRecordingState(RecordingState.initialising);
context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
await provider.initiateWebsocket(BleAudioCodec.pcm16, 16000);
// TODO: thinh, socket check why we need to close socket provider here, disable temporary
//context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
await provider.changeAudioRecordProfile(BleAudioCodec.pcm16, 16000);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The call to close the WebSocket without reconnecting has been commented out and replaced with a call to changeAudioRecordProfile. Make sure this doesn't introduce any unexpected behavior, especially if the WebSocket was being closed for a specific reason (e.g., to free up resources or prevent data leaks).

- context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
- await provider.initiateWebsocket(BleAudioCodec.pcm16, 16000);
+ //context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
+ await provider.changeAudioRecordProfile(BleAudioCodec.pcm16, 16000);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description Entelligence.AI

The initiateWebsocket method call has been replaced with changeAudioRecordProfile. Make sure that the new method correctly implements the required functionality and handles any potential errors.

- context.read<WebSocketProvider>().closeWebSocketWithoutReconnect('Recording with phone mic');
- await provider.initiateWebsocket(BleAudioCodec.pcm16, 16000);
+ await provider.changeAudioRecordProfile(BleAudioCodec.pcm16, 16000);

await provider.streamRecording();
MixpanelManager().phoneMicRecordingStarted();
},
Expand Down
Loading
Loading