mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Push-to-talk voice messaging using the Codec2 ultra-low-bitrate speech codec, transmitted as V: prefixed packets over the existing MeshCore LoRa mesh pipeline. Voice recording UI (long-press send button or + menu) is gated behind Platform.isIOS || Platform.isMacOS since the `record` package only supports microphone capture on those platforms in this build. Key changes: - VoiceRecorderService: streams 8kHz mono PCM chunks via `record` package - VoicePlayerService: decodes Codec2 bytes to WAV and plays via audioplayers - VoiceCodecService: async Codec2 encode/decode in background isolates - VoiceProvider: reassembles multi-packet sessions, drives playback - VoiceMessageBubble: shows packet progress, play/stop controls - MessagesProvider: detects V: prefix, routes to VoiceProvider - MessagesTab: PTT long-press gesture + recording indicator (iOS/macOS) - Message model: isVoice + voiceId fields for session tracking - Auto-selects codec mode from radio bandwidth (700C/1200/1300 bps) - codec2_flutter + meshcore_client switched from path to git deps
71 lines
2.1 KiB
Dart
71 lines
2.1 KiB
Dart
export 'package:meshcore_client/meshcore_client.dart'
|
|
show
|
|
Message,
|
|
MessageType,
|
|
MessageTextType,
|
|
MessageDeliveryStatus,
|
|
MessageRecipient;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:meshcore_client/meshcore_client.dart';
|
|
import 'sar_marker.dart';
|
|
import '../utils/voice_message_parser.dart';
|
|
|
|
extension MessageVoiceExtension on Message {
|
|
/// True when this message is a voice recording (`V:` text or binary voice).
|
|
bool get isVoiceMessage => isVoice;
|
|
|
|
/// Parses the voice packet mode from the stored [voiceId] session info.
|
|
/// Returns null for non-voice messages.
|
|
VoicePacketMode? get voicePacketMode {
|
|
if (!isVoice || text.isEmpty) return null;
|
|
final pkt = VoicePacket.tryParseText(text);
|
|
return pkt?.mode;
|
|
}
|
|
}
|
|
|
|
extension MessageSarExtension on Message {
|
|
/// Infer the [SarMarkerType] from stored SAR fields.
|
|
/// Returns null if this is not a SAR marker message.
|
|
SarMarkerType? get sarMarkerType {
|
|
if (!isSarMarker) return null;
|
|
|
|
if (sarCustomEmoji != null && sarCustomEmoji!.isNotEmpty) {
|
|
return SarMarkerType.fromEmoji(sarCustomEmoji!);
|
|
}
|
|
|
|
final trimmed = text.trim();
|
|
if (!trimmed.startsWith('S:')) return null;
|
|
|
|
final parts = trimmed.split(':');
|
|
if (parts.length < 3) return null;
|
|
|
|
return SarMarkerType.fromEmoji(parts[1]);
|
|
}
|
|
|
|
/// Convert to a [SarMarker] if this message contains SAR data.
|
|
SarMarker? toSarMarker() {
|
|
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
|
return null;
|
|
}
|
|
|
|
debugPrint('📍 [Message.toSarMarker] Converting to marker:');
|
|
debugPrint(' message.text: "$text"');
|
|
debugPrint(' message.sarNotes: "$sarNotes"');
|
|
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
|
debugPrint(' message.sarCustomEmoji: "$sarCustomEmoji"');
|
|
|
|
return SarMarker(
|
|
id: id,
|
|
type: sarMarkerType!,
|
|
location: sarGpsCoordinates!,
|
|
timestamp: sentAt,
|
|
senderPublicKey: senderPublicKeyPrefix,
|
|
senderName: senderName,
|
|
notes: sarNotes,
|
|
customEmoji: sarCustomEmoji,
|
|
colorIndex: sarColorIndex,
|
|
);
|
|
}
|
|
}
|