fix(ui,audio,ios,macos): eight P0 mobile fixes
User report from sideloaded iPhone build, in order of priority: #4 'Could not join channel: audio: audio backend: build_output_stream: The requested stream configuration is not supported by the device.' Cause: we forced cpal::BufferSize::Fixed(2048) on the output and input streams unconditionally on non-Linux. iOS CoreAudio RemoteIO units reject arbitrary buffer-size requests with that exact error. Windows WASAPI needs the pinning for shared-mode jitter, but macOS / iOS do not. Fix: cfg-gate Fixed(2048) to target_os = 'windows'; everywhere else use BufferSize::Default and let the platform HAL pick. crates/chanora_audio/src/engine.rs. #5 'Could not join channel: invariant violated: voice_in already taken' Cause: start_audio tore down the old engine BEFORE attempting to construct the new one, and consumed voice_in (an mpsc Receiver that can only be taken once) early. When the new engine failed mid-construction (e.g. because of #4 above) the session was left with: no audio engine, voice_in consumed, no way to retry without reconnect. The second voice_join attempt surfaced the invariant message. Fix: build the new engine BEFORE tearing down the old. Only swap state.audio if construction succeeded. crates/chanora_ core/src/lib.rs::ChanoraSession::start_audio. Additionally added a put_voice_in helper to the protocol adapter ( crates/chanora_protocol/src/adapter.rs) for a future broadcast-channel migration; the helper is unused on the immediate fix path but documents the intent. #3 'permission request would better on first open' Cause: AVAudioSession only triggers the mic-permission prompt the first time it tries to record. We never recorded until voice_join, so the prompt fired then. Fix iOS: AVAudioSession.sharedInstance().requestRecordPermission in AppDelegate.swift::application(_:didFinishLaunchingWithOptions:). Fix macOS: AVCaptureDevice.requestAccess(for: .audio) in macos/Runner/AppDelegate.swift::applicationDidFinishLaunching. Both run non-blocking; user can deny without crashing app launch, and voice_join then surfaces a clearer downstream error when the engine fails to open the input device. #1 + #2 'one-column upper takes too much space; Push to Talk button at bottom would be better' Layout rework for narrow-mode (single column, mobile shape): - Flipped the stacking order in main.dart so Voice Bar moves to the BOTTOM of the body and the channel tree (Expanded) fills above. Wide-mode (Row, >= 840 dp) layout unchanged. - Inside the Voice Bar on touch-only hosts, moved the on-screen Push to Talk button to be the LAST element of the Voice Bar (was Row 3). Order now: pill + mutes, mode badge + settings, level meter, stats line, release-tail caption, PTT button. The button is closest to the user's thumb when the Voice Bar is pinned to the bottom of a narrow-layout screen. #6 'remove right top debug badge' debugShowCheckedModeBanner: false on the MaterialApp. Release builds never showed it anyway; this only affects local dev / debug builds. #7 'what does the refresh button use for? nothing happened' Removed. The snapshot updates via BridgeEvent::SnapshotChanged are pushed from the bridge — a manual rust.snapshot() call was redundant. Now only the Diagnostics + Disconnect actions remain in the AppBar trailing row when connected. #8 'Bind Key related function should not be added to a mobile platform' widgets/voice_settings.dart: bind-key OutlinedButton is now #cfg'd out when Platform.isIOS || Platform.isAndroid. The release-tail slider stays because it still applies to the on-screen PTT button. Capability badge in voice_bar.dart also hidden on mobile (it would always show L0Focused which is redundant with the visible on-screen button). Tests + analyze: chanora_audio 34/0/0 on macOS, workspace 78/0/1 on Linux; flutter analyze clean (6 pre-existing Radio.groupValue infos). flutter build ios --release --no-codesign: 28.8 s clean (Runner.app 29.9 MB).
This commit is contained in:
@@ -229,32 +229,17 @@ class VoiceBar extends StatelessWidget {
|
||||
// Row 3: PTT-only secondary content.
|
||||
//
|
||||
// On hardware-keyboard hosts (Windows / macOS / Linux /
|
||||
// Web) this is a one-line bound-key + release-tail
|
||||
// hint.
|
||||
// Web) this is a one-line bound-key + release-tail hint
|
||||
// sitting right under the mode badge.
|
||||
//
|
||||
// On touch-only hosts (iOS / iPadOS / Android) there is
|
||||
// no hardware key to bind, so we replace the hint with
|
||||
// a touch-and-hold on-screen PTT button driven by
|
||||
// `_PttHoldButton`. The release-tail still applies; the
|
||||
// small print below the button shows it for parity
|
||||
// with the desktop hint line.
|
||||
if (isPtt && _isTouchOnlyPttHost) ...[
|
||||
const SizedBox(height: 8),
|
||||
_PttHoldButton(
|
||||
active: levelActive,
|
||||
onHeldChanged: onPttHeldChanged,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
'${l10n.voiceReleaseTailLabel}: $releaseTailMs${l10n.voiceReleaseTailHint}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (isPtt)
|
||||
// On touch-only hosts (iOS / iPadOS / Android) the
|
||||
// on-screen Push to Talk button is rendered AT THE
|
||||
// BOTTOM of the Voice Bar (see below) so it sits
|
||||
// closest to the user's thumb when the Voice Bar is
|
||||
// pinned to the bottom of a narrow-layout screen. The
|
||||
// release-tail value is folded into the small print
|
||||
// under the button rather than shown here.
|
||||
if (isPtt && !_isTouchOnlyPttHost)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 2),
|
||||
child: Text(
|
||||
@@ -288,12 +273,41 @@ class VoiceBar extends StatelessWidget {
|
||||
// bind-key flow through the Voice Bar's settings gear
|
||||
// (single configuration entry point — see the comment
|
||||
// on `onConfigure`).
|
||||
if (isPtt)
|
||||
//
|
||||
// Hidden on touch-only mobile hosts (iOS / iPadOS /
|
||||
// Android) because the capability story there is always
|
||||
// "L0Focused via on-screen button" and that's already
|
||||
// visually obvious from the PTT button being on the
|
||||
// bar. Showing a degraded-capability badge there would
|
||||
// be redundant + confusing.
|
||||
if (isPtt && !_isTouchOnlyPttHost)
|
||||
PttCapabilityBadge(
|
||||
level: pttLevel,
|
||||
backendId: pttBackendId,
|
||||
boundInputClass: pttBoundInputClass,
|
||||
),
|
||||
// On touch-only mobile hosts the Push to Talk button is
|
||||
// the LAST element of the Voice Bar so it lands closest
|
||||
// to the user's thumb when the Voice Bar is pinned to
|
||||
// the bottom of a narrow-layout screen. The release-
|
||||
// tail value sits above the button so the user sees
|
||||
// how long their voice continues after they let go.
|
||||
if (isPtt && _isTouchOnlyPttHost) ...[
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: Text(
|
||||
'${l10n.voiceReleaseTailLabel}: $releaseTailMs${l10n.voiceReleaseTailHint}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_PttHoldButton(
|
||||
active: levelActive,
|
||||
onHeldChanged: onPttHeldChanged,
|
||||
),
|
||||
],
|
||||
// Leave-voice button intentionally absent: TeamSpeak's
|
||||
// model is "user is always in some channel", not
|
||||
// Discord's join/leave-voice. To stop being heard /
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
// Voice settings dialog (SDD-097). Surfaces a TransmitMode radio
|
||||
// group, a bind-key button, and a release-tail slider.
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/generated/app_localizations.dart';
|
||||
import '../src/rust/api.dart' as rust;
|
||||
|
||||
/// True when the host is a touch-only mobile platform without a
|
||||
/// hardware keyboard the user would bind a PTT key on. Mirrors the
|
||||
/// helper in `voice_bar.dart`.
|
||||
bool get _isTouchOnlyPttHost {
|
||||
if (kIsWeb) return false;
|
||||
return Platform.isIOS || Platform.isAndroid;
|
||||
}
|
||||
|
||||
/// Result returned by [`VoiceSettingsDialog`]. `null` indicates a
|
||||
/// cancelled dialog.
|
||||
class VoiceSettingsResult {
|
||||
@@ -107,21 +118,30 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
||||
// them entirely when the user has switched to a
|
||||
// non-PTT mode so the dialog stays focused on what's
|
||||
// actually configurable for that mode.
|
||||
//
|
||||
// Additionally on touch-only mobile hosts (iOS / iPadOS
|
||||
// / Android) there is no hardware keyboard to bind a
|
||||
// key on — the VoiceBar renders an on-screen Push to
|
||||
// Talk button instead. Hide the Bind Key affordance
|
||||
// there but keep the release-tail slider since it
|
||||
// still applies to the on-screen button's behaviour.
|
||||
if (_mode == rust.BridgeTransmitMode.ptt) ...[
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.keyboard),
|
||||
label: Text(l10n.voiceBindKeyAction),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
VoiceSettingsResult(
|
||||
mode: _mode,
|
||||
releaseTailMs: _releaseTail.round(),
|
||||
bindKeyRequested: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!_isTouchOnlyPttHost) ...[
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.keyboard),
|
||||
label: Text(l10n.voiceBindKeyAction),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
VoiceSettingsResult(
|
||||
mode: _mode,
|
||||
releaseTailMs: _releaseTail.round(),
|
||||
bindKeyRequested: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
Text(
|
||||
l10n.voiceReleaseTailLabel,
|
||||
style: theme.textTheme.titleSmall,
|
||||
|
||||
Reference in New Issue
Block a user