ryanheise / audio_service

Flutter plugin to play audio in the background while the screen is off.
796 stars 474 forks source link

oneisolate branch: Seeking from ios lock screen #635

Closed esiqveland closed 3 years ago

esiqveland commented 3 years ago

Which API doesn't behave as documented, and how does it misbehave?

Seeking from lock screen on iOS is not working. Whenever I seek, the position is set to zero instead of the position I seeked to.

It is working correctly in lock screen for Android 11.

Minimal reproduction project The example

To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior:

  1. Start playing MediaItem
  2. Open lock screen on iOS
  3. Seek to a new position
  4. music starts playing from the beginning (position zero), but the seekbar on lock screen think it's playing from seek position. The progress bars remains out of sync until the next playback event occurs.
If applicable, copy & paste error message here, within the triple quotes to preserve formatting.

Expected behavior Seek to the position set from lock screen.

Screenshots If applicable, add screenshots to help explain your problem.

Runtime Environment (please complete the following information if relevant):

Flutter SDK version

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.0.3, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[✗] Chrome - develop for the web (Cannot find Chrome executable at
    google-chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[✓] Linux toolchain - develop for Linux desktop
[!] Android Studio (not installed)
[✓] Connected device (2 available)

Additional context

Versions of audio_service:

  audio_service:
    dependency: "direct main"
    description:
      path: audio_service
      ref: one-isolate
      resolved-ref: "9d0a2bdd0e1a57b366d1bff3ab1c728968555304"
      url: "https://github.com/ryanheise/audio_service.git"
    source: git
    version: "0.16.2"
  audio_service_platform_interface:
    dependency: transitive
    description:
      path: audio_service_platform_interface
      ref: one-isolate
      resolved-ref: "9d0a2bdd0e1a57b366d1bff3ab1c728968555304"
      url: "https://github.com/ryanheise/audio_service.git"
    source: git
    version: "1.0.0"
  audio_service_web:
    dependency: transitive
    description:
      path: audio_service_web
      ref: one-isolate
      resolved-ref: "9d0a2bdd0e1a57b366d1bff3ab1c728968555304"
      url: "https://github.com/ryanheise/audio_service.git"
    source: git
    version: "0.16.2"
  audio_session:
    dependency: transitive
    description:
      name: audio_session
      url: "https://pub.dartlang.org"
    source: hosted
    version: "0.1.0"

My exact AudioHandler:


class MyAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
  final _player = AudioPlayer();

  MyAudioHandler() {
    // Broadcast which item is currently playing
    // _player.currentIndexStream.listen((index) {
    //   if (index != null) {
    //     mediaItem.add(queue.value![index]);
    //   }
    // });

    // Broadcast the current playback state and what controls should currently
    // be visible in the media notification
    _player.playbackEventStream.listen((event) {
      playbackState.add(playbackState.value!.copyWith(
        controls: [
          MediaControl.skipToPrevious,
          _player.playing ? MediaControl.pause : MediaControl.play,
          MediaControl.skipToNext,
        ],
        androidCompactActionIndices: [0, 1, 2],
        systemActions: {
          MediaAction.seek,
          // MediaAction.seekForward,
          // MediaAction.seekBackward,
        },
        processingState: {
          ProcessingState.idle: AudioProcessingState.idle,
          ProcessingState.loading: AudioProcessingState.loading,
          ProcessingState.buffering: AudioProcessingState.buffering,
          ProcessingState.ready: AudioProcessingState.ready,
          ProcessingState.completed: AudioProcessingState.completed,
        }[_player.processingState],
        playing: _player.playing,
        updatePosition: _player.position,
        bufferedPosition: _player.bufferedPosition,
        speed: _player.speed,
      ));
    });

    // skip to next song when playback completes
    _player.playbackEventStream.listen((nextState) {
      if (_player.playing &&
          nextState.processingState == ProcessingState.completed) {
        skipToNext();
      }
    });
  }

  play() => _player.play();
  pause() => _player.pause();
  seek(Duration position) => _player.seek(position);
  seekTo(Duration position) => _player.seek(position);
  stop() async {
    await _player.stop();
    await super.stop();
  }

  @override
  Future<void> skipToNext() async {
    await _skip(1);
  }

  @override
  Future<void> skipToPrevious() async {
    await _skip(-1);
  }

  bool get hasNext {
    final queue = this.queue.value!;
    final index = playbackState.value!.queueIndex!;
    return queue.length > index + 1;
  }

  Future<void> _skip(int offset) async {
    final queue = this.queue.value!;
    final index = playbackState.value!.queueIndex!;
    if (index >= queue.length) {
      return;
    }
    if (index + offset < 0) {
      await skipToQueueItem(0);
      await seek(Duration.zero);
      return;
    }
    if (index + offset >= queue.length) {
      await pause();
      await skipToQueueItem(0);
      return;
    }
    final isPastStart = _player.position > Duration(milliseconds: 3500);
    if (offset == -1 && isPastStart) {
      await seek(Duration.zero);
      return;
    }
    await skipToQueueItem(index + offset);
  }

  @override
  Future<void> skipToQueueItem(int index) async {
    final q = queue.value!;
    await super.skipToQueueItem(index);
    var item = q[index];
    await _player.setAudioSource(await _toSource(item));
  }

  Future<void> customAction(
      String name, Map<String, dynamic>? arguments) async {
    switch (name) {
      case 'setVolume':
        _player.setVolume(arguments?['volume']);
        break;
      case 'saveBookmark':
        // app-specific code
        break;
    }
  }
}
ryanheise commented 3 years ago

Thanks for reporting, @esiqveland . I if I understand correctly, this is reproducible with the example, so you do not need to include a code snippet.

Can you confirm whether the example in the stable published version of audio_service exhibits the same bug as the example of the one-isolate branch?

esiqveland commented 3 years ago

I can confirm seeking from lock screen works in the example when ran from master branch.

Seeking from lock screen does not work in example project on one-isolate branch. Playback is set to zero, and the two playbars become out of sync until next playback event happens and moves the playback position on the lock screen to the new 'correct' position.

ryanheise commented 3 years ago

Does the same problem happen in the control center?

esiqveland commented 3 years ago

Yes

ryanheise commented 3 years ago

OK, I'm able to reproduce the lock screen issue on the Simulator, so hopefully what works for that will also work for the control center.

I noticed that the lock screen controls actually start working after issuing some state changes. I think I tried pausing first, then locking, and the controls then appeared to work (or maybe I also did a seek first).

So it is a matter of working out what is missing during initialisation.

esiqveland commented 3 years ago

Closing, as its fixed by #686

github-actions[bot] commented 2 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs, or use StackOverflow if you need help with audio_service.