audio_output_picker 0.0.1 copy "audio_output_picker: ^0.0.1" to clipboard
audio_output_picker: ^0.0.1 copied to clipboard

A powerful, cross-platform Flutter plugin to enumerate, monitor, and switch audio output devices and microphone inputs.

example/lib/main.dart

import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:audio_output_picker/audio_output_picker.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Audio & Mic Stream Picker',
      debugShowCheckedModeBanner: kDebugMode,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6366F1),
          brightness: Brightness.light,
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6366F1),
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system,
      home: const AudioPickerHomeScreen(),
    );
  }
}

class AudioPickerHomeScreen extends StatefulWidget {
  const AudioPickerHomeScreen({super.key});

  @override
  State<AudioPickerHomeScreen> createState() => _AudioPickerHomeScreenState();
}

class _AudioPickerHomeScreenState extends State<AudioPickerHomeScreen> {
  final _picker = AudioOutputPicker();
  final _audioPlayer = AudioPlayer();

  String _platformVersion = 'Unknown';
  bool _hasBtPermission = false;
  bool _hasMicPermission = false;

  // Active devices
  AudioOutputDevice? _currentOutput;
  AudioInputDevice? _currentMic;

  // Real-time stream subscriptions
  StreamSubscription<AudioOutputDevice?>? _currentOutputSub;
  StreamSubscription<AudioInputDevice?>? _currentMicSub;

  // Audio player state
  PlayerState _playerState = PlayerState.stopped;
  Duration _duration = Duration.zero;
  Duration _position = Duration.zero;

  late final StreamSubscription<PlayerState> _stateSub;
  late final StreamSubscription<Duration> _durationSub;
  late final StreamSubscription<Duration> _positionSub;

  @override
  void initState() {
    super.initState();
    _initPlatformAndPermissions();
    _initAudioPlayer();
    _initDeviceStreams();
  }

  void _initAudioPlayer() {
    _stateSub = _audioPlayer.onPlayerStateChanged.listen((state) {
      if (mounted) setState(() => _playerState = state);
    });

    _durationSub = _audioPlayer.onDurationChanged.listen((duration) {
      if (mounted) setState(() => _duration = duration);
    });

    _positionSub = _audioPlayer.onPositionChanged.listen((position) {
      if (mounted) setState(() => _position = position);
    });

    _audioPlayer.onPlayerComplete.listen((_) {
      if (mounted) {
        setState(() {
          _playerState = PlayerState.stopped;
          _position = Duration.zero;
        });
      }
    });

    _audioPlayer.setSource(AssetSource('sound/zense.mp3')).catchError((e) {
      debugPrint('Error loading asset audio: $e');
    });
  }

  Future<void> _initPlatformAndPermissions() async {
    String platformVersion;
    try {
      platformVersion =
          await _picker.getPlatformVersion() ?? 'Unknown platform';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    final hasBt = await _picker.checkBluetoothPermission();
    final hasMic = await _picker.checkMicrophonePermission();

    if (!mounted) return;
    setState(() {
      _platformVersion = platformVersion;
      _hasBtPermission = hasBt;
      _hasMicPermission = hasMic;
    });
  }

  void _initDeviceStreams() {
    _currentOutputSub = _picker.onCurrentOutputChanged.listen((device) {
      if (mounted) setState(() => _currentOutput = device);
    });

    _currentMicSub = _picker.onCurrentMicrophoneChanged.listen((mic) {
      if (mounted) setState(() => _currentMic = mic);
    });
  }

  Future<void> _togglePlayPause() async {
    if (_playerState == PlayerState.playing) {
      await _audioPlayer.pause();
    } else {
      await _audioPlayer.play(AssetSource('sound/zense.mp3'));
    }
  }

  Future<void> _stopAudio() async {
    await _audioPlayer.stop();
    setState(() => _position = Duration.zero);
  }

  Future<void> _requestBtPermission() async {
    final granted = await _picker.requestBluetoothPermission();
    if (!mounted) return;
    setState(() => _hasBtPermission = granted);
  }

  Future<void> _requestMicPermission() async {
    final granted = await _picker.requestMicrophonePermission();
    if (!mounted) return;
    setState(() => _hasMicPermission = granted);
  }

  Future<void> _openOutputPicker() async {
    final selected = await _picker.showAudioOutputPickerPopup(
      context: context,
      title: 'Audio Output',
      subtitle: 'Choose speaker, headphones, or Bluetooth output',
    );

    if (selected != null && mounted) {
      setState(() => _currentOutput = selected);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Switched output to: ${selected.name}'),
          behavior: SnackBarBehavior.floating,
          duration: const Duration(seconds: 2),
        ),
      );
    }
  }

  Future<void> _openMicrophonePicker() async {
    final selected = await _picker.showMicrophonePickerPopup(
      context: context,
      title: 'Microphone Input',
      subtitle: 'Choose recording / input source',
    );

    if (selected != null && mounted) {
      setState(() => _currentMic = selected);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Switched microphone to: ${selected.name}'),
          behavior: SnackBarBehavior.floating,
          duration: const Duration(seconds: 2),
        ),
      );
    }
  }

  IconData _getOutputIcon(AudioOutputType type) {
    switch (type) {
      case AudioOutputType.speaker:
        return Icons.volume_up_rounded;
      case AudioOutputType.receiver:
        return Icons.phone_in_talk_rounded;
      case AudioOutputType.wiredHeadphones:
        return Icons.headphones_rounded;
      case AudioOutputType.bluetooth:
        return Icons.bluetooth_audio_rounded;
      case AudioOutputType.usb:
        return Icons.usb_rounded;
      case AudioOutputType.airPlay:
        return Icons.airplay_rounded;
      case AudioOutputType.hdmi:
        return Icons.tv_rounded;
      case AudioOutputType.unknown:
        return Icons.speaker_group_rounded;
    }
  }

  IconData _getMicIcon(AudioInputType type) {
    switch (type) {
      case AudioInputType.builtInMic:
        return Icons.mic_rounded;
      case AudioInputType.wiredHeadsetMic:
        return Icons.headset_mic_rounded;
      case AudioInputType.bluetooth:
        return Icons.bluetooth_audio_rounded;
      case AudioInputType.usb:
        return Icons.usb_rounded;
      case AudioInputType.unknown:
        return Icons.mic_none_rounded;
    }
  }

  String _formatDuration(Duration d) {
    final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }

  @override
  void dispose() {
    _stateSub.cancel();
    _durationSub.cancel();
    _positionSub.cancel();
    _currentOutputSub?.cancel();
    _currentMicSub?.cancel();
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final isPlaying = _playerState == PlayerState.playing;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Audio & Microphone Streams'),
        elevation: 0,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
        physics: const BouncingScrollPhysics(),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Platform & Permissions
            _buildSystemBanner(theme, colorScheme),
            const SizedBox(height: 20),

            // 1. Audio Output Section (Headphones, Speaker, Bluetooth)
            _buildOutputSectionHeader(theme, colorScheme),
            const SizedBox(height: 12),
            _buildActiveOutputCard(theme, colorScheme),
            const SizedBox(height: 12),
            _buildOutputStreamView(theme, colorScheme),
            const SizedBox(height: 24),

            // 2. Microphone Section (Listenable Microphones)
            _buildMicrophoneSectionHeader(theme, colorScheme),
            const SizedBox(height: 12),
            _buildActiveMicrophoneCard(theme, colorScheme),
            const SizedBox(height: 12),
            _buildMicrophoneStreamView(theme, colorScheme),
            const SizedBox(height: 24),

            // 3. Audio Player Test
            _buildPlayerCard(theme, colorScheme, isPlaying),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget _buildSystemBanner(ThemeData theme, ColorScheme colorScheme) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: colorScheme.surfaceContainerHighest.withAlpha(120),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: colorScheme.outlineVariant.withAlpha(80)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(
                Icons.device_hub_rounded,
                size: 20,
                color: colorScheme.primary,
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  _platformVersion,
                  style: theme.textTheme.bodyMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ),
          const SizedBox(height: 10),
          Row(
            children: [
              Icon(
                _hasBtPermission
                    ? Icons.bluetooth_connected_rounded
                    : Icons.bluetooth_disabled_rounded,
                size: 16,
                color: _hasBtPermission ? Colors.green : colorScheme.error,
              ),
              const SizedBox(width: 6),
              Expanded(
                child: Text(
                  _hasBtPermission
                      ? 'Bluetooth OK'
                      : 'Bluetooth Permission Needed',
                  style: theme.textTheme.bodySmall,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              if (!_hasBtPermission)
                TextButton(
                  onPressed: _requestBtPermission,
                  style: TextButton.styleFrom(
                    visualDensity: VisualDensity.compact,
                    padding: const EdgeInsets.symmetric(horizontal: 8),
                  ),
                  child: const Text('Grant BT'),
                ),
            ],
          ),
          Row(
            children: [
              Icon(
                _hasMicPermission ? Icons.mic_rounded : Icons.mic_off_rounded,
                size: 16,
                color: _hasMicPermission ? Colors.green : colorScheme.error,
              ),
              const SizedBox(width: 6),
              Expanded(
                child: Text(
                  _hasMicPermission ? 'Microphone OK' : 'Mic Permission Needed',
                  style: theme.textTheme.bodySmall,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              if (!_hasMicPermission)
                TextButton(
                  onPressed: _requestMicPermission,
                  style: TextButton.styleFrom(
                    visualDensity: VisualDensity.compact,
                    padding: const EdgeInsets.symmetric(horizontal: 8),
                  ),
                  child: const Text('Grant Mic'),
                ),
            ],
          ),
        ],
      ),
    );
  }

  // ==========================================
  // Audio Outputs (Speaker, Headphones, Bluetooth)
  // ==========================================

  Widget _buildOutputSectionHeader(ThemeData theme, ColorScheme colorScheme) {
    return Row(
      children: [
        Icon(Icons.headphones_rounded, size: 20, color: colorScheme.primary),
        const SizedBox(width: 8),
        Expanded(
          child: Text(
            'AUDIO OUTPUT STREAM',
            style: theme.textTheme.labelMedium?.copyWith(
              fontWeight: FontWeight.bold,
              letterSpacing: 1.1,
              color: colorScheme.primary,
            ),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
          decoration: BoxDecoration(
            color: colorScheme.primary.withAlpha(25),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 6,
                height: 6,
                decoration: const BoxDecoration(
                  color: Colors.green,
                  shape: BoxShape.circle,
                ),
              ),
              const SizedBox(width: 5),
              Text(
                'Always Live',
                style: theme.textTheme.labelSmall?.copyWith(
                  color: colorScheme.primary,
                  fontWeight: FontWeight.w600,
                  fontSize: 11,
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildActiveOutputCard(ThemeData theme, ColorScheme colorScheme) {
    final current = _currentOutput;
    final deviceType = current?.type ?? AudioOutputType.speaker;

    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            colorScheme.primaryContainer,
            colorScheme.primaryContainer.withAlpha(160),
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(20),
      ),
      child: Column(
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: colorScheme.primary,
                  shape: BoxShape.circle,
                ),
                child: Icon(
                  _getOutputIcon(deviceType),
                  color: colorScheme.onPrimary,
                  size: 24,
                ),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'ACTIVE OUTPUT',
                      style: theme.textTheme.labelSmall?.copyWith(
                        color: colorScheme.onPrimaryContainer.withAlpha(180),
                        fontWeight: FontWeight.bold,
                        letterSpacing: 1.0,
                      ),
                    ),
                    const SizedBox(height: 2),
                    Text(
                      current?.name ?? 'Detecting Output...',
                      style: theme.textTheme.titleMedium?.copyWith(
                        color: colorScheme.onPrimaryContainer,
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    Text(
                      deviceType.toDisplayString(),
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: colorScheme.onPrimaryContainer.withAlpha(200),
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          FilledButton.icon(
            onPressed: _openOutputPicker,
            icon: const Icon(Icons.swap_horiz_rounded, size: 20),
            label: const Text(
              'Change Audio Output',
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            style: FilledButton.styleFrom(
              backgroundColor: colorScheme.primary,
              foregroundColor: colorScheme.onPrimary,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(14),
              ),
              minimumSize: const Size.fromHeight(44),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildOutputStreamView(ThemeData theme, ColorScheme colorScheme) {
    return StreamBuilder<List<AudioOutputDevice>>(
      stream: _picker.onOutputsChanged,
      builder: (context, snapshot) {
        final devices = snapshot.data ?? [];
        return Container(
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: colorScheme.surface,
            borderRadius: BorderRadius.circular(18),
            border: Border.all(color: colorScheme.outlineVariant.withAlpha(90)),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Icon(
                    Icons.sensors_rounded,
                    size: 16,
                    color: colorScheme.primary,
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      'Available Outputs Stream (${devices.length})',
                      style: theme.textTheme.labelMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              if (devices.isEmpty)
                Text(
                  'No output devices detected.',
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: colorScheme.onSurfaceVariant,
                    fontStyle: FontStyle.italic,
                  ),
                )
              else
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: devices.map((d) {
                    final isSelected = _currentOutput != null
                        ? d.id == _currentOutput?.id
                        : d.isSelected;
                    return InkWell(
                      onTap: () async {
                        await _picker.selectOutput(d);
                        setState(() => _currentOutput = d);
                      },
                      borderRadius: BorderRadius.circular(10),
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 10,
                          vertical: 6,
                        ),
                        decoration: BoxDecoration(
                          color: isSelected
                              ? colorScheme.primaryContainer
                              : colorScheme.surfaceContainerHighest.withAlpha(
                                  120,
                                ),
                          borderRadius: BorderRadius.circular(10),
                          border: Border.all(
                            color: isSelected
                                ? colorScheme.primary
                                : colorScheme.outlineVariant.withAlpha(80),
                          ),
                        ),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(
                              _getOutputIcon(d.type),
                              size: 16,
                              color: isSelected
                                  ? colorScheme.primary
                                  : colorScheme.onSurfaceVariant,
                            ),
                            const SizedBox(width: 6),
                            Flexible(
                              child: Text(
                                d.name,
                                style: theme.textTheme.bodySmall?.copyWith(
                                  fontWeight: isSelected
                                      ? FontWeight.bold
                                      : FontWeight.normal,
                                  color: isSelected
                                      ? colorScheme.onPrimaryContainer
                                      : colorScheme.onSurface,
                                ),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                            if (isSelected) ...[
                              const SizedBox(width: 4),
                              Icon(
                                Icons.check_circle_rounded,
                                size: 14,
                                color: colorScheme.primary,
                              ),
                            ],
                          ],
                        ),
                      ),
                    );
                  }).toList(),
                ),
            ],
          ),
        );
      },
    );
  }

  // ==========================================
  // Audio Inputs / Microphones
  // ==========================================

  Widget _buildMicrophoneSectionHeader(
    ThemeData theme,
    ColorScheme colorScheme,
  ) {
    return Row(
      children: [
        Icon(Icons.mic_rounded, size: 20, color: colorScheme.secondary),
        const SizedBox(width: 8),
        Expanded(
          child: Text(
            'MICROPHONE & INPUT STREAM',
            style: theme.textTheme.labelMedium?.copyWith(
              fontWeight: FontWeight.bold,
              letterSpacing: 1.1,
              color: colorScheme.secondary,
            ),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
          decoration: BoxDecoration(
            color: colorScheme.secondary.withAlpha(25),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 6,
                height: 6,
                decoration: const BoxDecoration(
                  color: Colors.green,
                  shape: BoxShape.circle,
                ),
              ),
              const SizedBox(width: 5),
              Text(
                'Always Live',
                style: theme.textTheme.labelSmall?.copyWith(
                  color: colorScheme.secondary,
                  fontWeight: FontWeight.w600,
                  fontSize: 11,
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildActiveMicrophoneCard(ThemeData theme, ColorScheme colorScheme) {
    final current = _currentMic;
    final micType = current?.type ?? AudioInputType.builtInMic;

    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            colorScheme.secondaryContainer,
            colorScheme.secondaryContainer.withAlpha(160),
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(20),
      ),
      child: Column(
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: colorScheme.secondary,
                  shape: BoxShape.circle,
                ),
                child: Icon(
                  _getMicIcon(micType),
                  color: colorScheme.onSecondary,
                  size: 24,
                ),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'ACTIVE MICROPHONE',
                      style: theme.textTheme.labelSmall?.copyWith(
                        color: colorScheme.onSecondaryContainer.withAlpha(180),
                        fontWeight: FontWeight.bold,
                        letterSpacing: 1.0,
                      ),
                    ),
                    const SizedBox(height: 2),
                    Text(
                      current?.name ?? 'Detecting Microphone...',
                      style: theme.textTheme.titleMedium?.copyWith(
                        color: colorScheme.onSecondaryContainer,
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    Text(
                      micType.toDisplayString(),
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: colorScheme.onSecondaryContainer.withAlpha(200),
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          FilledButton.icon(
            onPressed: _openMicrophonePicker,
            icon: const Icon(Icons.mic_external_on_rounded, size: 20),
            label: const Text(
              'Change Microphone Input',
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            style: FilledButton.styleFrom(
              backgroundColor: colorScheme.secondary,
              foregroundColor: colorScheme.onSecondary,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(14),
              ),
              minimumSize: const Size.fromHeight(44),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildMicrophoneStreamView(ThemeData theme, ColorScheme colorScheme) {
    return StreamBuilder<List<AudioInputDevice>>(
      stream: _picker.onMicrophonesChanged,
      builder: (context, snapshot) {
        final mics = snapshot.data ?? [];
        return Container(
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: colorScheme.surface,
            borderRadius: BorderRadius.circular(18),
            border: Border.all(color: colorScheme.outlineVariant.withAlpha(90)),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Icon(
                    Icons.hearing_rounded,
                    size: 16,
                    color: colorScheme.secondary,
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      'Listenable Microphones Stream (${mics.length})',
                      style: theme.textTheme.labelMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              if (mics.isEmpty)
                Text(
                  'No microphones detected.',
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: colorScheme.onSurfaceVariant,
                    fontStyle: FontStyle.italic,
                  ),
                )
              else
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: mics.map((m) {
                    final isSelected = _currentMic != null
                        ? m.id == _currentMic?.id
                        : m.isSelected;
                    return InkWell(
                      onTap: () async {
                        await _picker.selectInput(m);
                        setState(() => _currentMic = m);
                      },
                      borderRadius: BorderRadius.circular(10),
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 10,
                          vertical: 6,
                        ),
                        decoration: BoxDecoration(
                          color: isSelected
                              ? colorScheme.secondaryContainer
                              : colorScheme.surfaceContainerHighest.withAlpha(
                                  120,
                                ),
                          borderRadius: BorderRadius.circular(10),
                          border: Border.all(
                            color: isSelected
                                ? colorScheme.secondary
                                : colorScheme.outlineVariant.withAlpha(80),
                          ),
                        ),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(
                              _getMicIcon(m.type),
                              size: 16,
                              color: isSelected
                                  ? colorScheme.secondary
                                  : colorScheme.onSurfaceVariant,
                            ),
                            const SizedBox(width: 6),
                            Flexible(
                              child: Text(
                                m.name,
                                style: theme.textTheme.bodySmall?.copyWith(
                                  fontWeight: isSelected
                                      ? FontWeight.bold
                                      : FontWeight.normal,
                                  color: isSelected
                                      ? colorScheme.onSecondaryContainer
                                      : colorScheme.onSurface,
                                ),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                            if (isSelected) ...[
                              const SizedBox(width: 4),
                              Icon(
                                Icons.check_circle_rounded,
                                size: 14,
                                color: colorScheme.secondary,
                              ),
                            ],
                          ],
                        ),
                      ),
                    );
                  }).toList(),
                ),
            ],
          ),
        );
      },
    );
  }

  // ==========================================
  // Audio Player Test
  // ==========================================

  Widget _buildPlayerCard(
    ThemeData theme,
    ColorScheme colorScheme,
    bool isPlaying,
  ) {
    final maxDuration = _duration.inMilliseconds.toDouble();
    final currentPos = _position.inMilliseconds.toDouble().clamp(
      0.0,
      maxDuration > 0 ? maxDuration : 1.0,
    );

    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: colorScheme.surface,
        borderRadius: BorderRadius.circular(24),
        border: Border.all(color: colorScheme.outlineVariant.withAlpha(100)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(15),
            blurRadius: 20,
            offset: const Offset(0, 8),
          ),
        ],
      ),
      child: Column(
        children: [
          Row(
            children: [
              Container(
                width: 56,
                height: 56,
                decoration: BoxDecoration(
                  gradient: const LinearGradient(
                    colors: [Color(0xFF6366F1), Color(0xFFA855F7)],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  ),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: const Icon(
                  Icons.music_note_rounded,
                  color: Colors.white,
                  size: 30,
                ),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'zense.mp3',
                      style: theme.textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 4),
                    Row(
                      children: [
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 2,
                          ),
                          decoration: BoxDecoration(
                            color: colorScheme.secondaryContainer,
                            borderRadius: BorderRadius.circular(6),
                          ),
                          child: Text(
                            'assets/sound/',
                            style: theme.textTheme.labelSmall?.copyWith(
                              color: colorScheme.onSecondaryContainer,
                              fontFamily: 'monospace',
                            ),
                          ),
                        ),
                        const SizedBox(width: 8),
                        if (isPlaying)
                          Row(
                            children: [
                              Icon(
                                Icons.graphic_eq_rounded,
                                size: 16,
                                color: colorScheme.primary,
                              ),
                              const SizedBox(width: 4),
                              Text(
                                'Playing',
                                style: theme.textTheme.labelSmall?.copyWith(
                                  color: colorScheme.primary,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ],
                          ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 18),

          SliderTheme(
            data: SliderTheme.of(context).copyWith(
              trackHeight: 4,
              thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
              overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
            ),
            child: Slider(
              value: currentPos,
              max: maxDuration > 0 ? maxDuration : 1.0,
              onChanged: (value) {
                _audioPlayer.seek(Duration(milliseconds: value.toInt()));
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  _formatDuration(_position),
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: colorScheme.onSurfaceVariant,
                  ),
                ),
                Text(
                  _formatDuration(_duration),
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 14),

          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton.outlined(
                tooltip: 'Replay',
                icon: const Icon(Icons.replay_rounded),
                onPressed: () async {
                  await _audioPlayer.seek(Duration.zero);
                  await _audioPlayer.play(AssetSource('sound/zense.mp3'));
                },
              ),
              const SizedBox(width: 16),
              Container(
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: colorScheme.primary,
                  boxShadow: [
                    BoxShadow(
                      color: colorScheme.primary.withAlpha(70),
                      blurRadius: 16,
                      offset: const Offset(0, 6),
                    ),
                  ],
                ),
                child: IconButton(
                  iconSize: 36,
                  tooltip: isPlaying ? 'Pause' : 'Play',
                  color: colorScheme.onPrimary,
                  icon: Icon(
                    isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
                  ),
                  onPressed: _togglePlayPause,
                ),
              ),
              const SizedBox(width: 16),
              IconButton.outlined(
                tooltip: 'Stop',
                icon: const Icon(Icons.stop_rounded),
                onPressed: _stopAudio,
              ),
            ],
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
98
downloads

Documentation

API reference

Publisher

verified publisherhandeaktan.com.tr

Weekly Downloads

A powerful, cross-platform Flutter plugin to enumerate, monitor, and switch audio output devices and microphone inputs.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on audio_output_picker

Packages that implement audio_output_picker