Files
buzz/mobile/lib/shared/relay/media_upload/helpers.dart
klopez4212 6f5ec4a731 Add mobile voice notes (#7121)
## Summary
- add iOS and Android voice-note recording and preview directly in the
mobile composer
- add waveform playback, scrubbing, speed controls, haptics, and
one-shot playback in chat
- package recordings in a canonical H.264/AAC MP4 envelope on both
platforms so existing relays accept them
- preserve the shared composer interaction and attachment-card treatment
across mobile platforms

Mobile counterpart to #6978.

## Testing
- `just ci`
- `just mobile-check`
- `just mobile-test` (2,026 tests)
- Android debug build compiled, installed, launched, and Voice note
verified in the attachment menu on Pixel 10
- signed iOS device build installed on iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-09-03 08:07:04 +00:00

140 lines
3.8 KiB
Dart

part of '../media_upload.dart';
bool _startsWith(Uint8List bytes, List<int> prefix) {
if (bytes.length < prefix.length) return false;
for (var i = 0; i < prefix.length; i++) {
if (bytes[i] != prefix[i]) return false;
}
return true;
}
bool _matchesAscii(Uint8List bytes, int offset, String value) {
final codeUnits = ascii.encode(value);
if (bytes.length < offset + codeUnits.length) return false;
for (var i = 0; i < codeUnits.length; i++) {
if (bytes[offset + i] != codeUnits[i]) return false;
}
return true;
}
int _readUint32BigEndian(Uint8List bytes, int offset) {
return (bytes[offset] << 24) |
(bytes[offset + 1] << 16) |
(bytes[offset + 2] << 8) |
bytes[offset + 3];
}
int _readUint32LittleEndian(Uint8List bytes, int offset) {
return bytes[offset] |
(bytes[offset + 1] << 8) |
(bytes[offset + 2] << 16) |
(bytes[offset + 3] << 24);
}
Future<Uint8List?> _readPlatformClipboardImage() async {
return _mediaUploadPlatformChannel.invokeMethod<Uint8List>(
_readClipboardImageMethod,
);
}
Future<Uint8List?> _generatePickedVideoPoster(String filePath) {
return _mediaUploadPlatformChannel.invokeMethod<Uint8List>(
_generateVideoPosterMethod,
filePath,
);
}
Future<String> _transcodePickedVideoToMp4(String filePath) async {
final result = await _mediaUploadPlatformChannel.invokeMethod<String>(
_transcodeVideoToMp4Method,
filePath,
);
if (result == null || result.isEmpty) {
throw Exception('Failed to convert video to MP4.');
}
if (defaultTargetPlatform == TargetPlatform.android) {
final source = File(result);
final destination = File(
'$result.faststart-${DateTime.now().microsecondsSinceEpoch}.mp4',
);
try {
await rewriteMp4ForFastStart(source, destination);
await source.delete();
return destination.path;
} catch (_) {
try {
await destination.delete();
} on FileSystemException {
// Best-effort cleanup; preserve the original platform error.
}
rethrow;
}
}
return result;
}
Future<String> _packagePickedVoiceNoteForUpload(String filePath) async {
final result = await _mediaUploadPlatformChannel.invokeMethod<String>(
_packageVoiceNoteForUploadMethod,
filePath,
);
if (result == null || result.isEmpty) {
throw Exception('Failed to prepare voice note for upload.');
}
if (defaultTargetPlatform == TargetPlatform.android) {
final source = File(result);
final destination = File(
'$result.faststart-${DateTime.now().microsecondsSinceEpoch}.mp4',
);
try {
await rewriteMp4ForFastStart(source, destination);
await source.delete();
return destination.path;
} catch (_) {
for (final file in [destination, source]) {
try {
if (await file.exists()) await file.delete();
} on FileSystemException {
// Best-effort cleanup; preserve the original platform error.
}
}
rethrow;
}
}
return result;
}
Future<Uint8List> _transcodePickedImageToJpeg(Uint8List bytes) async {
return _invokeRequiredPlatformBytesMethod(
_transcodeImageToJpegMethod,
arguments: bytes,
errorMessage: 'failed to convert image for upload',
);
}
Future<Uint8List> _sanitizePickedImageBytes(
Uint8List bytes,
String mimeType,
) async {
return _invokeRequiredPlatformBytesMethod(
_sanitizeImageForUploadMethod,
arguments: {'bytes': bytes, 'mimeType': mimeType},
errorMessage: 'failed to sanitize image for upload',
);
}
Future<Uint8List> _invokeRequiredPlatformBytesMethod(
String method, {
Object? arguments,
required String errorMessage,
}) async {
final result = await _mediaUploadPlatformChannel.invokeMethod<Uint8List>(
method,
arguments,
);
if (result == null || result.isEmpty) {
throw Exception(errorMessage);
}
return result;
}