Flutter audioplayers seek when dragging not working on iOS Lock Screen

Viewed 231

I am using audioplayers plugin on iOS.

When I have the audio playing and lock the screen, the audio continues to play in the background and the iOS Lock Screen display is showing.

enter image description here

However when I drag the player slider forward on the iOS Lock Screen,it jumps back and resets to the beginning of the audio track, it does not seek to the dragged point.

import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers/notifications.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hello_now/models/activity.dart';
import 'package:hello_now/services/user_service.dart';

class PlayerWidget extends StatefulWidget {
  final String url;
  final PlayerMode mode;
  final bool isChecked;
  final Function checkBoxCallback;
  final VoidCallback? onPlayerFinished;
  final String name;
  final String title;
  final int? fileDuration;

  const PlayerWidget(
      {Key? key,
      required this.url,
      required this.title,
      this.mode = PlayerMode.MEDIA_PLAYER,
      required this.name,
      required this.isChecked,
      required this.checkBoxCallback,
      required this.onPlayerFinished,
      this.fileDuration})
      : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PlayerWidgetState(url, mode, isChecked);
  }
}

class _PlayerWidgetState extends State<PlayerWidget> {
  String url;
  PlayerMode mode;
  bool isFinished;
  bool showProgress = false;

  late final Function callback;
  late AudioPlayer _audioPlayer;
  PlayerState? _audioPlayerState;
  Duration? _duration;
  Duration? _position;

  bool _isSeeking = false;

  Color SliderColor = Color(0xFFEDAF99);

  bool shouldShowFirst = true;

  PlayerState _playerState = PlayerState.STOPPED;
  PlayingRoute _playingRouteState = PlayingRoute.SPEAKERS;
  StreamSubscription? _durationSubscription;
  StreamSubscription? _positionSubscription;
  StreamSubscription? _playerCompleteSubscription;
  StreamSubscription? _playerErrorSubscription;
  StreamSubscription? _playerStateSubscription;
  StreamSubscription<PlayerControlCommand>? _playerControlCommandSubscription;
  StreamSubscription? _playerSeekSubscription;

  bool get _isPlaying => _playerState == PlayerState.PLAYING;
  bool get _isPaused => _playerState == PlayerState.PAUSED;
  String get _durationText {
    return _duration?.toString().split('.').first ?? '';
  }

  String get _positionText => _position?.toString().split('.').first ?? '';

  bool get _isPlayingThroughEarpiece =>
      _playingRouteState == PlayingRoute.EARPIECE;

  void callbackHandler(state) {
    print('State change $state');
  }

  _PlayerWidgetState(this.url, this.mode, this.isFinished);

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

  @override
  void dispose() {
    saveDuration();
    _audioPlayer.dispose();
    _audioPlayer.notificationService.clearNotification();
    _durationSubscription?.cancel();
    _positionSubscription?.cancel();
    _playerCompleteSubscription?.cancel();
    _playerErrorSubscription?.cancel();
    _playerStateSubscription?.cancel();
    _playerControlCommandSubscription?.cancel();
    _playerSeekSubscription?.cancel();
    super.dispose();
  }

  Future<void> saveDuration() async {
    Activity activity = new Activity();
    activity.durationCompleted = await _audioPlayer.getCurrentPosition();
    activity.uid = FirebaseAuth.instance.currentUser!.email;
    DateTime currentDate = DateTime.now(); //DateTime
    Timestamp myTimeStamp = Timestamp.fromDate(currentDate);
    activity.dateTime = myTimeStamp;
    activity.practiceType = widget.title;
    var x = activity.durationCompleted! / 1000;
    var seconds = x % 60;
    UserService userService = new UserService();
    userService.addActivity(activity);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Stack(
                children: [
                  SliderTheme(
                    data: SliderTheme.of(context).copyWith(
                      activeTrackColor: SliderColor,
                      inactiveTrackColor: SliderColor.withOpacity(0.5),
                      trackShape: RectangularSliderTrackShape(),
                      trackHeight: 4.0,
                      thumbColor: SliderColor,
                      thumbShape:
                          RoundSliderThumbShape(enabledThumbRadius: 12.0),
                      overlayColor: SliderColor.withOpacity(0.5),
                      overlayShape:
                          RoundSliderOverlayShape(overlayRadius: 15.0),
                    ),
                    child: Slider(
                SliderColor.withOpacity(0.5),
                      onChanged: (v) {
                        print("onChanged");

                        final duration = _duration;
                        if (duration == null) {
                          return;
                        }

                        final Position = v * duration.inSeconds;

                        setState(() {
                          _audioPlayer
                              .seek(Duration(seconds: Position.round()));
                        });

                      },
                      onChangeEnd: (v) {
                        print("onChangeEnd");
                        final duration = _duration;
                        if (duration == null) {
                          return;
                        }

                        final Position = v * duration.inSeconds;


                        _audioPlayer.seek(Duration(seconds: Position.round()));
                      },
                      value: (_position != null &&
                              _duration != null &&
                              _position!.inMilliseconds > 0 &&
                              _position!.inMilliseconds <
                                  _duration!.inMilliseconds)
                          ?
                        
                          _position!.inSeconds / _duration!.inSeconds
                          : 0.0,
                    ),
                  ),
                ],
              ),
            ),

            Text(
              _position != null
                  ? '${_printDuration(_position!)} / ${_printDuration(_duration!)}'
                  : _duration != null
                      ? _durationText
                      : '',
              style: const TextStyle(fontSize: 18.0),
            ),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                AnimatedCrossFade(
                  crossFadeState: shouldShowFirst
                      ? CrossFadeState.showFirst
                      : CrossFadeState.showSecond,
                  duration: const Duration(milliseconds: 200),
                  firstChild: IconButton(
                    splashColor: Colors.transparent,
                    key: const Key('play_button'),
                    onPressed: _isPlaying ? null : _play,
                    iconSize: 64.0,
                    icon: const Icon(Icons.play_arrow),
                    color: SliderColor,
                  ),
                  secondChild: IconButton(
                    splashColor: Colors.transparent,
                    key: const Key('pause_button'),
                    onPressed: _isPlaying ? _pause : null,
                    iconSize: 64.0,
                    icon: const Icon(Icons.pause),
                    color: SliderColor,
                  ),
                ),
              ],
            ),
          ],
        ),
      ],
    );
  }

  void _initAudioPlayer() {
    _audioPlayer = AudioPlayer(mode: mode);

    _durationSubscription = _audioPlayer.onDurationChanged.listen((duration) {
      setState(() => _duration = duration);

      if (Theme.of(context).platform == TargetPlatform.iOS) {
        _audioPlayer.notificationService.startHeadlessService();

        _audioPlayer.notificationService.setNotification(
          title: widget.title,
          artist: widget.name,
          albumTitle: 'Hello Now',
          imageUrl:
              'https://static.wixstatic.com/media/c59891_e7c14f57cc2d42059734f4b44dc8426c~mv2.png/v1/fill/w_260,h_170,al_c,usm_0.66_1.00_0.01/HelloNow-Logo-Stacked-Black%20(1).png',
          forwardSkipInterval: const Duration(seconds: 30), // default is 30s
          backwardSkipInterval: const Duration(seconds: 30), // default is 30s
          duration: duration,
          enableNextTrackButton: true,
          enablePreviousTrackButton: true,
        );
      }
    });

    _playerSeekSubscription = _audioPlayer.onSeekComplete.listen((finished) {
      _audioPlayer.onAudioPositionChanged.listen((p) => setState(() {
            _position = p;
          }));
    });

    _positionSubscription =
        _audioPlayer.onAudioPositionChanged.listen((p) => setState(() {
              _position = p;
            }));

    _playerCompleteSubscription =
        _audioPlayer.onPlayerCompletion.listen((event) {
      _onComplete();
      widget.onPlayerFinished!();
      setState(() {
        _position = _duration;
        shouldShowFirst = !shouldShowFirst;
      });
    });

    _playerErrorSubscription = _audioPlayer.onPlayerError.listen((msg) {
      print('audioPlayer error : $msg');
      setState(() {
        _playerState = PlayerState.STOPPED;
        _duration = const Duration();
        _position = const Duration();
      });
    });

    _playerControlCommandSubscription =
        _audioPlayer.notificationService.onPlayerCommand.listen((command) {
      _audioPlayer.onAudioPositionChanged.listen((p) => setState(() {
            _position = p;
          }));

      print('command: $command');
    });

    _audioPlayer.onPlayerStateChanged.listen((state) {
      if (mounted) {
        setState(() {
          _audioPlayerState = state;
        });
      }
    });

    _audioPlayer.onNotificationPlayerStateChanged.listen((state) {
      if (mounted) {
        setState(() => _audioPlayerState = state);
      }
    });

    _playingRouteState = PlayingRoute.SPEAKERS;
  }

  Future<int> _play() async {
    final playPosition = (_position != null &&
            _duration != null &&
            _position!.inMilliseconds > 0 &&
            _position!.inMilliseconds < _duration!.inMilliseconds)
        ? _position
        : null;

    int result = await _audioPlayer.play(url, position: playPosition);

    if (result == 1) {
      setState(() {
        _playerState = PlayerState.PLAYING;
        shouldShowFirst = !shouldShowFirst;
      });
    }

    return result;
  }

  Future<int> _pause() async {
    final result = await _audioPlayer.pause();
    if (result == 1) {
      setState(() {
        _playerState = PlayerState.PAUSED;
        shouldShowFirst = !shouldShowFirst;
      });
    }
    return result;
  }

  Future<int> _earpieceOrSpeakersToggle() async {
    final result = await _audioPlayer.earpieceOrSpeakersToggle();
    if (result == 1) {
      setState(() => _playingRouteState = _playingRouteState.toggle());
    }
    return result;
  }

  Future<int> _stop() async {
    final result = await _audioPlayer.stop();
    if (result == 1) {
      setState(() {
        _playerState = PlayerState.STOPPED;
        _position = const Duration();
      });
    }
    return result;
  }

  void _onComplete() {
    setState(() => _playerState = PlayerState.STOPPED);
  }
}

String _printDuration(Duration duration) {
  String twoDigits(int n) => n.toString().padLeft(2, "0");
  String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
  String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
  return "$twoDigitMinutes:$twoDigitSeconds";
}
1 Answers

Your Slider has both onChanged and onChangeEnd doing a seek. I'm going to focus on onChangeEnd and suggest removing onChanged. It's not clear whether having both would cause some sort of state conflict within the audio player given these are async tasks being performed to seek, and you really only want to update once the user has chosen a spot rather than trying to do it in real time. We also aren't updating state so there's no need for the setState call inside.

onChangeEnd: (v) async {
  print("onChangeEnd");
  final duration = _duration;
  if (duration == null) {
    return;
  }
  await _audioPlayer.seek(duration * v);
},
Related