Files
buzz/mobile/test/shared/huddle/huddle_media_test.dart
klopez4212 4baccd5394 Downgrade mobile Huddles to audio protocol v2 (#6558)
## Summary

- downgrade Mobile Huddle authentication and native media configuration
from protocol v3 to the currently deployed relay's v2 contract
- restore the released one-byte relay peer prefix while retaining later
reconnect, roster, and playout-reset reliability fixes
- update Android, iOS, protocol documentation, and focused tests
together

Protocol v2 does not carry v3's occupancy epoch on audio frames, so it
cannot fence the narrow delayed-packet/peer-index-reuse race. This is an
intentional compatibility tradeoff until the relay v3 rollout is ready.

### Related issue

None found.

### Testing

- `just mobile-check`
- `just mobile-test` — 1,661 tests passed
- Android debug build installed and launched on Pixel 10 as
`xyz.block.buzz.mobile.sprout_mobile_profile_settings`; foreground
process verified
- signed iOS Release build installed and launched on iPhone as
`com.buzz.buzzMobile`; running process verified

A live two-device Huddle audio call remains a manual verification step.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-22 18:37:08 +01:00

172 lines
5.5 KiB
Dart

import 'package:buzz/shared/huddle/huddle_media.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel(huddleMediaMethodChannelName);
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
tearDown(() {
messenger.setMockMethodCallHandler(channel, null);
});
test(
'prepares the fixed v2 audio session without overstating codecs',
() async {
final calls = <MethodCall>[];
messenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return switch (call.method) {
'getCapabilities' => {
'platform': 'android',
'audioSession': true,
'microphonePermission': true,
'capture': false,
'playback': false,
'opusEncoding': false,
'opusDecoding': false,
},
'requestMicrophonePermission' => 'granted',
'prepare' => {'audioSessionPrepared': true},
'stop' => null,
_ => throw MissingPluginException(),
};
});
final media = MethodChannelHuddleMedia(channel: channel);
addTearDown(media.dispose);
final capabilities = await media.discoverCapabilities();
expect(capabilities.supportsAudioSession, isTrue);
expect(capabilities.supportsFullDuplexOpus, isFalse);
expect(
await media.requestMicrophonePermission(),
HuddleMicrophonePermission.granted,
);
await media.prepare();
expect(media.state.phase, HuddleMediaPhase.prepared);
final prepare = calls.singleWhere((call) => call.method == 'prepare');
expect(prepare.arguments, {
'protocolVersion': 2,
'sampleRateHz': 48000,
'channels': 1,
'frameSamples': 960,
});
await expectLater(
media.start(),
throwsA(
isA<HuddleMediaError>().having(
(error) => error.code,
'code',
HuddleMediaErrorCode.unsupported,
),
),
);
expect(media.state.phase, HuddleMediaPhase.prepared);
},
);
test('reports a missing native implementation as unavailable', () async {
final media = MethodChannelHuddleMedia(channel: channel);
addTearDown(media.dispose);
final capabilities = await media.discoverCapabilities();
expect(capabilities, same(HuddleMediaCapabilities.unavailable));
expect(media.state.phase, HuddleMediaPhase.unavailable);
});
test('opens system settings and reports the native launch result', () async {
var invoked = 0;
messenger.setMockMethodCallHandler(channel, (call) async {
invoked += 1;
expect(call.method, 'openSystemSettings');
return true;
});
final media = MethodChannelHuddleMedia(channel: channel);
addTearDown(media.dispose);
expect(await media.openSystemSettings(), isTrue);
expect(invoked, 1);
});
test('missing native settings support degrades to not launched', () async {
final media = MethodChannelHuddleMedia(channel: channel);
addTearDown(media.dispose);
expect(await media.openSystemSettings(), isFalse);
});
test('starts unmuted and forwards direct mute and speaker changes', () async {
final calls = <MethodCall>[];
messenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return switch (call.method) {
'getCapabilities' => {
'platform': 'android',
'audioSession': true,
'microphonePermission': true,
'capture': true,
'playback': true,
'opusEncoding': true,
'opusDecoding': true,
},
'prepare' => {'audioSessionPrepared': true},
'start' || 'setMuted' || 'setSpeakerEnabled' || 'stop' => null,
_ => throw MissingPluginException(),
};
});
final media = MethodChannelHuddleMedia(channel: channel);
addTearDown(media.dispose);
await media.discoverCapabilities();
await media.prepare();
await media.start();
expect(media.state.isMuted, isFalse);
expect(media.state.isSpeakerEnabled, isFalse);
await media.setMuted(true);
expect(media.state.phase, HuddleMediaPhase.active);
expect(media.state.isMuted, isTrue);
expect(calls.singleWhere((call) => call.method == 'setMuted').arguments, {
'muted': true,
});
await media.setSpeakerEnabled(true);
expect(media.state.isSpeakerEnabled, isTrue);
expect(
calls.singleWhere((call) => call.method == 'setSpeakerEnabled').arguments,
{'enabled': true},
);
});
test('parses aggregate capture diagnostics without audio content', () {
final diagnostics = HuddleCaptureDiagnostics.fromMap({
'frameCount': 50,
'rmsDbovHistogram': {'-42': 30, '-36': 20},
'peakDbovHistogram': {'-24': 40, '-18': 10},
'maxPeakDbov': -18,
'audioSource': 7,
'audioSourceLabel': 'voice_communication',
'deviceId': 4,
'deviceType': 1,
'deviceLabel': 'Pixel microphone',
'acousticEchoCancelerAvailable': true,
'acousticEchoCancelerEnabled': true,
'noiseSuppressorAvailable': true,
'noiseSuppressorEnabled': true,
'automaticGainControlAvailable': true,
'automaticGainControlEnabled': false,
});
expect(diagnostics.rmsDbovHistogram, {-42: 30, -36: 20});
expect(diagnostics.peakDbovHistogram, {-24: 40, -18: 10});
expect(diagnostics.maxPeakDbov, -18);
expect(diagnostics.debugSummary, contains('voice_communication'));
});
}