audio_service icon indicating copy to clipboard operation
audio_service copied to clipboard

Platform player ad2b050d-a01b-49f3-a3d7-c7be8cd4f197 already exists

Open Sedatdynn opened this issue 5 months ago • 1 comments

Platforms exhibiting the bug

  • [x] Android
  • [ ] iOS
  • [ ] web

Devices exhibiting the bug

There is no issue during normal usage, but when I try to play media files quickly one after another, I encounter this problem. Even though I dispose of the currently defined player, a new one is not created properly. The duration of the media I want to play remains at 0.0, and the issue does not resolve unless I completely restart the app.

Minimal reproduction project

Minimal reproduction project
class AudioPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
  AudioPlayer _player = AudioPlayer();

  // Stream subscriptions
  StreamSubscription<PlayerState>? _playerStateSubscription;
  StreamSubscription<Duration?>? _durationSubscription;
  StreamSubscription<Duration>? _positionSubscription;

  AudioPlayerHandler() {
    _init();
  }

  void _init() {
    _playerStateSubscription = _player.playerStateStream.listen(_handlePlayerStateChange);

    _durationSubscription = _player.durationStream.listen((duration) {
      if (duration != null) {
        mediaItem.add(mediaItem.value?.copyWith(duration: duration));
      }
    });

    _positionSubscription = _player.positionStream.listen((position) {
      playbackState.add(playbackState.value.copyWith(
        updatePosition: position,
      ));
    });
  }

  void _handlePlayerStateChange(PlayerState state) {
    final playing = state.playing;
    final processingState = state.processingState;
    final hasNext = locator.call<PlaylistProvider>().nextSong != null;
    final hasPrevious = locator.call<PlaylistProvider>().previousSong != null;

    AudioProcessingState audioProcessingState;
    switch (processingState) {
      case ProcessingState.idle:
        audioProcessingState = AudioProcessingState.idle;
        break;
      case ProcessingState.loading:
      case ProcessingState.buffering:
        audioProcessingState = AudioProcessingState.loading;
        break;
      case ProcessingState.ready:
        audioProcessingState = AudioProcessingState.ready;
        break;
      case ProcessingState.completed:
        audioProcessingState = AudioProcessingState.completed;
        break;
    }

    playbackState.add(PlaybackState(
      controls: [
        if (hasPrevious) MediaControl.skipToPrevious,
        if (playing) MediaControl.pause else MediaControl.play,
        if (hasNext) MediaControl.skipToNext,
      ],
      systemActions: const {
        MediaAction.seek,
        MediaAction.seekForward,
        MediaAction.seekBackward,
        MediaAction.rewind,
        MediaAction.fastForward,
        MediaAction.skipToPrevious,
        MediaAction.skipToNext,
      },
      androidCompactActionIndices: const [0, 1, 2],
      processingState: audioProcessingState,
      playing: playing,
      updatePosition: _player.position,
      bufferedPosition: _player.bufferedPosition,
      speed: _player.speed,
      queueIndex: 0,
    ));
  }

  @override
  Future<void> play() async {
    try {
      await _player.play();
    } catch (e) {
      locator<Logger>().error('Play error: $e');
      rethrow;
    }
  }

  @override
  Future<void> pause() async {
    try {
      await _player.pause();
    } catch (e) {
      locator<Logger>().error('Pause error: $e');
      rethrow;
    }
  }

  @override
  Future<void> stop() async {
    try {
      await _player.stop();
      playbackState.add(PlaybackState(
        processingState: AudioProcessingState.idle,
        playing: false,
      ));
    } catch (e) {
      locator<Logger>().error('Stop error: $e');
      rethrow;
    }
  }

  @override
  Future<void> seek(Duration position) async {
    try {
      await _player.seek(position);
    } catch (e) {
      locator<Logger>().error('Seek error: $e');
      rethrow;
    }
  }

  // Custom method to set audio source
  Future<void> setAudioSource(AudioSource source, {Duration? initialPosition}) async {
    try {
      await _player.setAudioSource(source, initialPosition: initialPosition);
    } catch (e) {
      locator<Logger>().error('Set audio source error: $e');
      rethrow;
    }
  }

  // Custom method to set media item
  void setMediaItem(MediaItem item) {
    mediaItem.add(item);
  }

  // Getters for accessing player state
  bool get playing => _player.playing;
  ProcessingState get processingState => _player.processingState;
  Duration get position => _player.position;
  Duration get bufferedPosition => _player.bufferedPosition;
  Duration? get duration => _player.duration;

  // Stream getters
  Stream<PlayerState> get playerStateStream => _player.playerStateStream;
  Stream<Duration> get positionStream => _player.positionStream;
  Stream<Duration?> get durationStream => _player.durationStream;
  Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream;

  @override
  Future<void> onTaskRemoved() async {
    await stop();
  }


  Future<void> recreatePlayer() async {
    try {
      await _onDestroy();
      // _init();
    } catch (e) {
      locator<Logger>().error('Recreate player error: $e');
    } finally {
      _player = AudioPlayer(handleInterruptions: false);
      _init();
    }
  }

  Future<void> resetPlayerSafely() async {
    locator.call<Logger>.call().error('CustomCrashlytics resetPlayerSafely called!');
    try {
      await _player.stop();
      await _player.dispose();
    } catch (e) {
      locator.call<Logger>.call().error('Error resetting player: $e');
    } finally {
      locator.call<Logger>.call().error('CustomCrashlytics resetPlayerSafely finally called!');
      _player = AudioPlayer(handleInterruptions: false);
    }
  }

  @override
  Future<void> click([MediaButton button = MediaButton.media]) async {
    switch (button) {
      case MediaButton.media:
        if (_player.playing) {
          await pause();
        } else {
          await play();
        }
        break;
      case MediaButton.next:
        await skipToNext();
        break;
      case MediaButton.previous:
        await skipToPrevious();
        break;
    }
  }

  Future<void> _onDestroy() async {
    await _playerStateSubscription?.cancel();
    await _durationSubscription?.cancel();
    await _positionSubscription?.cancel();
    await _player.stop();
    await _player.dispose();
  }

  @override
  Future<void> skipToNext() async {
    try {
      final playlistProvider = locator.call<PlaylistProvider>();
      final nextSong = playlistProvider.nextSong;
      if (nextSong != null) {
        final mp3Provider = locator.call<MP3Provider>();
        mp3Provider.playMP3(nextSong, ActionType.change);
      }
    } catch (e) {
      locator<Logger>().error('Skip to next error: $e');
    }
  }

  @override
  Future<void> skipToPrevious() async {
    try {
      final playlistProvider = locator.call<PlaylistProvider>();
      final previousSong = playlistProvider.previousSong;
      if (previousSong != null) {
        final mp3Provider = locator.call<MP3Provider>();
        mp3Provider.playMP3(previousSong, ActionType.change);
      }
    } catch (e) {
      locator<Logger>().error('Skip to previous error: $e');
    }
  }
}


initialize:
   await AudioService.init(
        builder: () => locator.call<AudioPlayerHandler>.call(),
        config: const AudioServiceConfig(
          androidNotificationChannelId: 'com..background_player',
          androidNotificationChannelName: 'customPlayer',
          androidNotificationOngoing: true,
          androidStopForegroundOnPause: true,
          androidNotificationIcon: 'drawable/splash',
        ),
      );

Steps to reproduce

try to play media files quickly one after another (or call play-stop quickly)

Expected results

There is no issue during normal usage, but when I try to play media files quickly one after another, I encounter this problem. Even though I dispose of the currently defined player, a new one is not created properly. The duration of the media I want to play remains at 0.0, and the issue does not resolve unless I completely restart the app.

Actual results

Getting error: Platform player already exists PlatformException(Platform player ad2b050d-a01b-49f3-a3d7-c7be8cd4f197 already exists, null, null, null)

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste here]

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.22.0, on Microsoft Windows [Version 10.0.26100.4770], locale tr-TR)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[!] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.12.4)
[√] Android Studio (version 2024.2)
[√] VS Code (version 1.102.1)
[√] Connected device (4 available)
[√] Network resources

! Doctor found issues in 1 category.

Sedatdynn avatar Aug 04 '25 06:08 Sedatdynn

This is from just_audio, not audio_service. The issue is concurrent calls to player.dispose() and player.stop().

You should reuse the same AudioPlayer instance. If you are done with it, only call dispose(), which implicitly calls stop(). If you must recreate the instance for whatever reason, try this:

final previousPlayer = _player;
_player = AudioPlayer();
await previousPlayer.dispose();

Colton127 avatar Oct 17 '25 03:10 Colton127