content: // ignore_for_file: public_member_api_docs, avoid_print
  
  // This example demonstrates:
  //
  // - queues/playlists
  // - switching between audio handlers (audio player / text-to-speech player)
  // - logging
  // - custom actions
  // - custom events
  // - Android 11 media session resumption
  // - Android Auto
  //
  // To run this example, use:
  //
  // flutter run -t lib/example_multiple_handlers.dart
  
  import 'dart:async';
  
  import 'package:audio_service/audio_service.dart';
  import 'package:audio_service_example/common.dart';
  import 'package:audio_session/audio_session.dart';
  import 'package:flutter/foundation.dart';
  import 'package:flutter/material.dart';
  import 'package:flutter_tts/flutter_tts.dart';
  import 'package:just_audio/just_audio.dart';
  import 'package:rxdart/rxdart.dart';
  
  // You might want to provide this using dependency injection rather than a
  // global variable.
  late AudioHandler _audioHandler;
  
  /// Extension methods for our custom actions.
  extension DemoAudioHandler on AudioHandler {
    Future<void> switchToHandler(int? index) async {
      if (index == null) return;
      await _audioHandler
          .customAction('switchToHandler', <String, dynamic>{'index': index});
    }
  }
  
  Future<void> main() async {
    _audioHandler = await AudioService.init(
      builder: () => LoggingAudioHandler(MainSwitchHandler([
        AudioPlayerHandler(),
        TextPlayerHandler(),
      ])),
      config: const AudioServiceConfig(
        androidNotificationChannelId: 'com.ryanheise.myapp.channel.audio',
        androidNotificationChannelName: 'Audio playback',
        androidNotificationOngoing: true,
      ),
    );
    // Log async errors from the platform.
    AudioService.asyncError.listen((e) => print('Async error: $e'));
    runApp(const MyApp());
  }
  
  /// The app widget
  class MyApp extends StatelessWidget {
    const MyApp({super.key});
  
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Audio Service Demo',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: const MainScreen(),
      );
    }
  }
  
  /// The main screen.
  class MainScreen extends StatelessWidget {
    static const _handlerNames = [
      'Audio Player',
      'Text-To-Speech',
    ];
  
    const MainScreen({super.key});
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Audio Service Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Queue display/controls.
              StreamBuilder<QueueState>(
                stream: _queueStateStream,
                builder: (context, snapshot) {
                  final queueState = snapshot.data;
                  final queue = queueState?.queue ?? const [];
                  final mediaItem = queueState?.mediaItem;
                  return Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      StreamBuilder<dynamic>(
                        stream: _audioHandler.customState,
                        builder: (context, snapshot) {
                          final handlerIndex =
                              (snapshot.data?.handlerIndex as int?) ?? 0;
                          return DropdownButton<int>(
                            value: handlerIndex,
                            items: [
                              for (var i = 0; i < _handlerNames.length; i++)
                                DropdownMenuItem<int>(
                                  value: i,
                                  child: Text(_handlerNames[i]),
                                ),
                            ],
                            onChanged: _audioHandler.switchToHandler,
                          );
                        },
                      ),
                      if (queue.isNotEmpty)
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            IconButton(
                              icon: const Icon(Icons.skip_previous),
                              iconSize: 64.0,
                              onPressed: mediaItem == queue.first
                                  ? null
                                  : _audioHandler.skipToPrevious,
                            ),
                            IconButton(
                              icon: const Icon(Icons.skip_next),
                              iconSize: 64.0,
                              onPressed: mediaItem == queue.last
                                  ? null
                                  : _audioHandler.skipToNext,
                            ),
                          ],
                        ),
                      if (mediaItem?.title != null) Text(mediaItem!.title),
                    ],
                  );
                },
              ),
              // Play/pause/stop buttons.
              StreamBuilder<bool>(
                stream: _audioHandler.playbackState
                    .map((state) => state.playing)
                    .distinct(),
                builder: (context, snapshot) {
                  final playing = snapshot.data ?? false;
                  return Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      if (playing) _pauseButton() else _playButton(),
                      _stopButton(),
                    ],
                  );
                },
              ),
              // A seek bar.
              StreamBuilder<MediaState>(
                stream: _mediaStateStream,
                builder: (context, snapshot) {
                  final mediaState = snapshot.data;
                  return SeekBar(
                    duration: mediaState?.mediaItem?.duration ?? Duration.zero,
                    position: mediaState?.position ?? Duration.zero,
                    onChangeEnd: (newPosition) {
                      _audioHandler.seek(newPosition);
                    },
                  );
                },
              ),
              // Display the processing state.
              StreamBuilder<AudioProcessingState>(
                stream: _audioHandler.playbackState
                    .map((state) => state.processingState)
                    .distinct(),
                builder: (context, snapshot) {
                  final processingState =
                      snapshot.data ?? AudioProcessingState.idle;
                  return Text(
                      // ignore: deprecated_member_use
                      "Processing state: ${describeEnum(processingState)}");
                },
              ),
              // Display the latest custom event.
              StreamBuilder<dynamic>(
                stream: _audioHandler.customEvent,
                builder: (context, snapshot) {
                  return Text("custom event: ${snapshot.data}");
                },
              ),
              // Display the notification click status.
              StreamBuilder<bool>(
                stream: AudioService.notificationClicked,
                builder: (context, snapshot) {
                  return Text(
                    'Notification Click Status: ${snapshot.data}',
                  );
                },
              ),
            ],
          ),
        ),
      );
    }
  
    /// A stream reporting the combined state of the current media item and its
    /// current position.
    Stream<MediaState> get _mediaStateStream =>
        Rx.combineLatest2<MediaItem?, Duration, MediaState>(
            _audioHandler.mediaItem,
            AudioService.position,
            (mediaItem, position) => MediaState(mediaItem, position));
  
    /// A stream reporting the combined state of the current queue and the current
    /// media item within that queue.
    Stream<QueueState> get _queueStateStream =>
        Rx.combineLatest2<List<MediaItem>, MediaItem?, QueueState>(
            _audioHandler.queue,
            _audioHandler.mediaItem,
            (queue, mediaItem) => QueueState(queue, mediaItem));
  
    IconButton _playButton() => IconButton(
          icon: const Icon(Icons.play_arrow),
          iconSize: 64.0,
          onPressed: _audioHandler.play,
        );
  
    IconButton _pauseButton() => IconButton(
          icon: const Icon(Icons.pause),
          iconSize: 64.0,
          onPressed: _audioHandler.pause,
        );
  
    IconButton _stopButton() => IconButton(
          icon: const Icon(Icons.stop),
          iconSize: 64.0,
          onPressed: _audioHandler.stop,
        );
  }
  
  class QueueState {
    final List<MediaItem> queue;
    final MediaItem? mediaItem;
  
    QueueState(this.queue, this.mediaItem);
  }
  
  class MediaState {
    final MediaItem? mediaItem;
    final Duration position;
  
    MediaState(this.mediaItem, this.position);
  }
  
  class CustomEvent {
    final int handlerIndex;
  
    CustomEvent(this.handlerIndex);
  }
  
  class MainSwitchHandler extends SwitchAudioHandler {
    final List<AudioHandler> handlers;
    @override
    BehaviorSubject<dynamic> customState =
        BehaviorSubject<dynamic>.seeded(CustomEvent(0));
  
    MainSwitchHandler(this.handlers) : super(handlers.first) {
      // Configure the app's audio category and attributes for speech.
      AudioSession.instance.then((session) {
        session.configure(const AudioSessionConfiguration.speech());
      });
    }
  
    @override
    Future<dynamic> customAction(String name,
        [Map<String, dynamic>? extras]) async {
      switch (name) {
        case 'switchToHandler':
          stop();
          final index = extras!['index'] as int;
          inner = handlers[index];
          customState.add(CustomEvent(index));
          return null;
        default:
          return super.customAction(name, extras);
      }
    }
  }
  
  /// An [AudioHandler] for playing a list of podcast episodes.
  class AudioPlayerHandler extends BaseAudioHandler
      with QueueHandler, SeekHandler {
    // ignore: close_sinks
    final BehaviorSubject<List<MediaItem>> _recentSubject =
        BehaviorSubject.seeded(<MediaItem>[]);
    final _mediaLibrary = MediaLibrary();
    final _player = AudioPlayer();
  
    int? get index => _player.currentIndex;
  
    AudioPlayerHandler() {
      _init();
    }
  
    Future<void> _init() async {
      // Load and broadcast the queue
      queue.add(_mediaLibrary.items[MediaLibrary.albumsRootId]!);
      // For Android 11, record the most recent item so it can be resumed.
      mediaItem
          .whereType<MediaItem>()
          .listen((item) => _recentSubject.add([item]));
      // Broadcast media item changes.
      _player.currentIndexStream.listen((index) {
        if (index != null) mediaItem.add(queue.value[index]);
      });
      // Propagate all events from the audio player to AudioService clients.
      _player.playbackEventStream.listen(_broadcastState);
      // In this example, the service stops when reaching the end.
      _player.processingStateStream.listen((state) {
        if (state == ProcessingState.completed) stop();
      });
      try {
        // After a cold restart (on Android), _player.load jumps straight from
        // the loading state to the completed state. Inserting a delay makes it
        // work. Not sure why!
        //await Future.delayed(Duration(seconds: 2)); // magic delay
        await _player.setAudioSource(ConcatenatingAudioSource(
          children: queue.value
              .map((item) => AudioSource.uri(Uri.parse(item.id)))
              .toList(),
        ));
      } catch (e) {
        print("Error: $e");
      }
    }
  
    @override
    Future<List<MediaItem>> getChildren(String parentMediaId,
        [Map<String, dynamic>? options]) async {
      switch (parentMediaId) {
        case AudioService.recentRootId:
          // When the user resumes a media session, tell the system what the most
          // recently played item was.
          return _recentSubject.value;
        default:
          // Allow client to browse the media library.
          return _mediaLibrary.items[parentMediaId]!;
      }
    }
  
    @override
    ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) {
      switch (parentMediaId) {
        case AudioService.recentRootId:
          final stream = _recentSubject.map((_) => <String, dynamic>{});
          return _recentSubject.hasValue
              ? stream.shareValueSeeded(<String, dynamic>{})
              : stream.shareValue();
        default:
          return Stream.value(_mediaLibrary.items[parentMediaId])
              .map((_) => <String, dynamic>{})
              .shareValue();
      }
    }
  
    @override
    Future<void> skipToQueueItem(int index) async {
      // Then default implementations of skipToNext and skipToPrevious provided by
      // the [QueueHandler] mixin will delegate to this method.
      if (index < 0 || index >= queue.value.length) return;
      // This jumps to the beginning of the queue item at newIndex.
      _player.seek(Duration.zero, index: index);
      // Demonstrate custom events.
      customEvent.add('skip to $index');
    }
  
    @override
    Future<void> play() => _player.play();
  
    @override
    Future<void> pause() => _player.pause();
  
    @override
    Future<void> seek(Duration position) => _player.seek(position);
  
    @override
    Future<void> stop() async {
      await _player.stop();
      await playbackState.firstWhere(
          (state) => state.processingState == AudioProcessingState.idle);
    }
  
    /// Broadcasts the current state to all clients.
    void _broadcastState(PlaybackEvent event) {
      final playing = _player.playing;
      playbackState.add(playbackState.value.copyWith(
        controls: [
          MediaControl.skipToPrevious,
          if (playing) MediaControl.pause else MediaControl.play,
          MediaControl.stop,
          MediaControl.skipToNext,
        ],
        systemActions: const {
          MediaAction.seek,
          MediaAction.seekForward,
          MediaAction.seekBackward,
        },
        androidCompactActionIndices: const [0, 1, 3],
        processingState: const {
          ProcessingState.idle: AudioProcessingState.idle,
          ProcessingState.loading: AudioProcessingState.loading,
          ProcessingState.buffering: AudioProcessingState.buffering,
          ProcessingState.ready: AudioProcessingState.ready,
          ProcessingState.completed: AudioProcessingState.completed,
        }[_player.processingState]!,
        playing: playing,
        updatePosition: _player.position,
        bufferedPosition: _player.bufferedPosition,
        speed: _player.speed,
        queueIndex: event.currentIndex,
      ));
    }
  }
  
  /// Provides access to a library of media items. In your app, this could come
  /// from a database or web service.
  class MediaLibrary {
    static const albumsRootId = 'albums';
  
    final items = <String, List<MediaItem>>{
      AudioService.browsableRootId: const [
        MediaItem(
          id: albumsRootId,
          title: "Albums",
          playable: false,
        ),
      ],
      albumsRootId: [
        MediaItem(
          id: 'https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3',
          album: "Science Friday",
          title: "A Salute To Head-Scratching Science",
          artist: "Science Friday and WNYC Studios",
          duration: const Duration(milliseconds: 5739820),
          artUri: Uri.parse(
              'https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg'),
        ),
        MediaItem(
          id: 'https://s3.amazonaws.com/scifri-segments/scifri201711241.mp3',
          album: "Science Friday",
          title: "From Cat Rheology To Operatic Incompetence",
          artist: "Science Friday and WNYC Studios",
          duration: const Duration(milliseconds: 2856950),
          artUri: Uri.parse(
              'https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg'),
        ),
      ],
    };
  }
  
  /// This task defines logic for speaking a sequence of numbers using
  /// text-to-speech.
  class TextPlayerHandler extends BaseAudioHandler with QueueHandler {
    final _tts = Tts();
    final _sleeper = Sleeper();
    Completer<void>? _completer;
    var _index = 0;
    bool _interrupted = false;
    var _running = false;
  
    bool get _playing => playbackState.value.playing;
  
    TextPlayerHandler() {
      _init();
    }
  
    Future<void> _init() async {
      final session = await AudioSession.instance;
      // Handle audio interruptions.
      session.interruptionEventStream.listen((event) {
        if (event.begin) {
          if (_playing) {
            pause();
            _interrupted = true;
          }
        } else {
          switch (event.type) {
            case AudioInterruptionType.pause:
            case AudioInterruptionType.duck:
              if (!_playing && _interrupted) {
                play();
              }
              break;
            case AudioInterruptionType.unknown:
              break;
          }
          _interrupted = false;
        }
      });
      // Handle unplugged headphones.
      session.becomingNoisyEventStream.listen((_) {
        if (_playing) pause();
      });
      queue.add(List.generate(
          10,
          (i) => MediaItem(
                id: 'tts_${i + 1}',
                album: 'Numbers',
                title: 'Number ${i + 1}',
                artist: 'Sample Artist',
                extras: <String, int>{'number': i + 1},
                duration: const Duration(seconds: 1),
              )));
    }
  
    Future<void> run() async {
      _completer = Completer<void>();
      _running = true;
      while (_running) {
        try {
          if (_playing) {
            mediaItem.add(queue.value[_index]);
            playbackState.add(playbackState.value.copyWith(
              updatePosition: Duration.zero,
              queueIndex: _index,
            ));
            AudioService.androidForceEnableMediaButtons();
            await Future.wait([
              _tts.speak('${mediaItem.value!.extras!["number"]}'),
              _sleeper.sleep(const Duration(seconds: 1)),
            ]);
            if (_index + 1 < queue.value.length) {
              _index++;
            } else {
              _running = false;
            }
          } else {
            await _sleeper.sleep();
          }
          // ignore: empty_catches
        } on SleeperInterruptedException {
          // ignore: empty_catches
        } on TtsInterruptedException {}
      }
      _index = 0;
      mediaItem.add(queue.value[_index]);
      playbackState.add(playbackState.value.copyWith(
        updatePosition: Duration.zero,
      ));
      if (playbackState.value.processingState != AudioProcessingState.idle) {
        stop();
      }
      _completer?.complete();
      _completer = null;
    }
  
    @override
    Future<void> skipToQueueItem(int index) async {
      _index = index;
      _signal();
    }
  
    @override
    Future<void> play() async {
      if (_playing) return;
      final session = await AudioSession.instance;
      // flutter_tts doesn't activate the session, so we do it here. This
      // allows the app to stop other apps from playing audio while we are
      // playing audio.
      if (await session.setActive(true)) {
        // If we successfully activated the session, set the state to playing
        // and resume playback.
        playbackState.add(playbackState.value.copyWith(
          controls: [MediaControl.pause, MediaControl.stop],
          processingState: AudioProcessingState.ready,
          playing: true,
        ));
        if (_completer == null) {
          run();
        } else {
          _sleeper.interrupt();
        }
      }
    }
  
    @override
    Future<void> pause() async {
      _interrupted = false;
      playbackState.add(playbackState.value.copyWith(
        controls: [MediaControl.play, MediaControl.stop],
        processingState: AudioProcessingState.ready,
        playing: false,
      ));
      _signal();
    }
  
    @override
    Future<void> stop() async {
      playbackState.add(playbackState.value.copyWith(
        controls: [],
        processingState: AudioProcessingState.idle,
        playing: false,
      ));
      _running = false;
      _signal();
      // Wait for the speech to stop
      await _completer?.future;
      // Shut down this task
      await super.stop();
    }
  
    void _signal() {
      _sleeper.interrupt();
      _tts.interrupt();
    }
  }
  
  /// An object that performs interruptable sleep.
  class Sleeper {
    Completer<void>? _blockingCompleter;
  
    /// Sleep for a duration. If sleep is interrupted, a
    /// [SleeperInterruptedException] will be thrown.
    Future<void> sleep([Duration? duration]) async {
      _blockingCompleter = Completer();
      if (duration != null) {
        await Future.any<void>(
            [Future.delayed(duration), _blockingCompleter!.future]);
      } else {
        await _blockingCompleter!.future;
      }
      final interrupted = _blockingCompleter!.isCompleted;
      _blockingCompleter = null;
      if (interrupted) {
        throw SleeperInterruptedException();
      }
    }
  
    /// Interrupt any sleep that's underway.
    void interrupt() {
      if (_blockingCompleter?.isCompleted == false) {
        _blockingCompleter!.complete();
      }
    }
  }
  
  class SleeperInterruptedException {}
  
  /// A wrapper around FlutterTts that makes it easier to wait for speech to
  /// complete.
  class Tts {
    final FlutterTts _flutterTts = FlutterTts();
    Completer<void>? _speechCompleter;
    bool _interruptRequested = false;
    bool _playing = false;
  
    Tts() {
      _flutterTts.setCompletionHandler(() {
        _speechCompleter?.complete();
      });
    }
  
    bool get playing => _playing;
  
    Future<void> speak(String text) async {
      _playing = true;
      if (!_interruptRequested) {
        _speechCompleter = Completer();
        await _flutterTts.speak(text);
        await _speechCompleter!.future;
        _speechCompleter = null;
      }
      _playing = false;
      if (_interruptRequested) {
        _interruptRequested = false;
        throw TtsInterruptedException();
      }
    }
  
    Future<void> stop() async {
      if (_playing) {
        await _flutterTts.stop();
        _speechCompleter?.complete();
      }
    }
  
    void interrupt() {
      if (_playing) {
        _interruptRequested = true;
        stop();
      }
    }
  }
  
  class TtsInterruptedException {}
  