Search code examples
flutterdartsharedpreferences

Flutter save and restore AudioPlayer position


I'm using audioplayers library in flutter and I'm trying to save and restore the player position to except playing from the first position, like with caching player seek bar position, in this code, I tried to save and restore that with SharedPreference but my implemented is unsuccessful

class ApplicationSettings {
  ApplicationSettings(StreamingSharedPreferences preferences)
      : showIntro = preferences.getBool('showIntro', defaultValue: false),
        pageViewIndex = preferences.getInt('pageViewIndex', defaultValue: 0),
        audioPosition = preferences.getString('audioPosition', defaultValue: "{}")
  ;

  final Preference<bool> showIntro;
  final Preference<int> pageViewIndex;
  final Preference<String> audioPosition;
}

AudioInformation class:

part'audio_information.g.dart';

@JsonSerializable(nullable: true)
class AudioInformation {
  final String productName;
  final int audioPosition;

  AudioInformation(this.productName, this.audioPosition);

  factory AudioInformation.fromJson(Map<String, dynamic> json) => _$AudioInformationFromJson(json);

  Map<String, dynamic> toJson() => _$AudioInformationToJson(this);
}

PlayerWidget class:

enum PlayerState { stopped, playing, paused }
enum PlayingRouteState { speakers, earpiece }

class PlayerWidget extends StatefulWidget {
  final String url;
  final PlayerMode mode;
  final String productName;
  final String imageUrl;

  PlayerWidget({Key key, @required this.url, this.mode = PlayerMode.MEDIA_PLAYER, @required this.productName, @required this.imageUrl}) : super(key: key);

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

class _PlayerWidgetState extends State<PlayerWidget> {
  _PlayerWidgetState(this.url, this.mode);

  String url;
  PlayerMode mode;

  AudioPlayer _audioPlayer;
  Duration _duration;
  Duration _position;

  PlayerState _playerState = PlayerState.stopped;
  PlayingRouteState _playingRouteState = PlayingRouteState.speakers;
  StreamSubscription _durationSubscription;
  StreamSubscription _positionSubscription;
  StreamSubscription _playerCompleteSubscription;
  StreamSubscription _playerErrorSubscription;
  StreamSubscription _playerStateSubscription;
  StreamSubscription<PlayerControlCommand> _playerControlCommandSubscription;

  get _isPlaying => _playerState == PlayerState.playing;

  get _isPaused => _playerState == PlayerState.paused;

  get _durationText => _duration?.toString()?.split('.')?.first ?? '';

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

  Preference<String> _audioPosition;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _audioPosition = Provider.of<ApplicationSettings>(context).audioPosition;
  }

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

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

  @override
  Widget build(BuildContext context) {
    return PreferenceBuilder(
        preference: _audioPosition,
        builder: (context, String audioDetail) {

          /* SAVE position*/
          AudioInformation _audio = AudioInformation('${widget.productName}',  _duration?.inMilliseconds?.round()??0);
          _audioPosition.setValue(_audio.toJson().toString());

          return Scaffold(
            appBar: AppBar(
              title: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Icon(Icons.audiotrack_outlined),
                  Expanded(
                    child: Text(
                      ' - ${widget.productName}',
                      overflow: TextOverflow.ellipsis,
                      maxLines: 1,
                    ),
                  ),
                ],
              ),
            ),
            body: Stack(
              children: [
                Positioned.fill(
                    child: CachedNetworkImage(
                  imageUrl: widget.imageUrl,
                  fit: BoxFit.cover,
                )),
                Container(
                  width: double.infinity,
                  height: double.infinity,
                  child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
                    child: Container(
                      color: Colors.white.withOpacity(0.7),
                    ),
                  ),
                ),
                Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Container(
                      margin: EdgeInsets.all(16.0),
                      decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(11.0),
                          boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), offset: Offset(0.0, 0.0), spreadRadius: 1.0)],
                          border: Border.all(color: Colors.black)),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(10.0),
                        child: CachedNetworkImage(
                          imageUrl: '${widget.imageUrl}',
                          fit: BoxFit.cover,
                          width: 150.0,
                        ),
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.all(8.0),
                      padding: EdgeInsets.all(5.0),
                      decoration: BoxDecoration(color: Colors.white.withOpacity(0.5), border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(5.0)),
                      child: Column(
                        children: [
                          Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            crossAxisAlignment: CrossAxisAlignment.center,
                            children: [
                              IconButton(
                                key: Key('play_button'),
                                onPressed: _isPlaying ? null : () => _play(),
                                iconSize: 64.0,
                                icon: Icon(MdiIcons.playCircle),
                                color: Colors.black,
                              ),
                              IconButton(
                                key: Key('pause_button'),
                                onPressed: _isPlaying ? () => _pause() : null,
                                iconSize: 64.0,
                                icon: Icon(MdiIcons.pauseCircle),
                                color: Colors.green[900],
                              ),
                              IconButton(
                                key: Key('stop_button'),
                                onPressed: _isPlaying || _isPaused ? () => _stop() : null,
                                iconSize: 64.0,
                                icon: Icon(MdiIcons.stopCircle),
                                color: Colors.indigo[700],
                              ),
                            ],
                          ),
                          Slider(
                            onChanged: (v) {
                              final position = v * _duration.inMilliseconds;
                              _audioPlayer.seek(Duration(milliseconds: position.round()));

                              /* SAVE position*/
                              AudioInformation _audio = AudioInformation('${widget.productName}', position.round());
                              _audioPosition.setValue(_audio.toJson().toString());
                            },
                            value: (_position != null && _duration != null && _position.inMilliseconds > 0 && _position.inMilliseconds < _duration.inMilliseconds)
                                ? _position.inMilliseconds / _duration.inMilliseconds
                                : 0.0,
                          ),
                        ],
                      ),
                    ),
                    _durationText != null && _durationText.toString().isNotEmpty
                        ? Container(
                            height: 43.0,
                            padding: EdgeInsets.all(8.0),
                            decoration: BoxDecoration(color: Colors.white.withOpacity(0.5), border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(5.0)),
                            child: Text(
                              _position != null
                                  ? '${_positionText ?? ''} / ${_durationText ?? ''}'
                                  : _duration != null
                                      ? _durationText
                                      : ' --- ',
                              style: TextStyle(fontSize: 24.0),
                            ),
                          )
                        : Container(
                            height: 43.0,
                          ),
                  ],
                ),
              ],
            ),
          );
        });
  }

  void _initAudioPlayer() {
    _audioPlayer = AudioPlayer(mode: mode);
    _durationSubscription = _audioPlayer.onDurationChanged.listen((duration) {
      setState(() => _duration = duration);
    });

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

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

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

    _playerControlCommandSubscription = _audioPlayer.onPlayerCommand.listen((command) {
      print('command');
    });

    _audioPlayer.onPlayerStateChanged.listen((state) {
      if (!mounted) return;
    });

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

    _playingRouteState = PlayingRouteState.speakers;
  }

  Future<int> _play() async {
    final playPosition = (_position != null && _duration != null && _position.inMilliseconds > 0 && _position.inMilliseconds < _duration.inMilliseconds) ? _position : null;
    final result = await _audioPlayer.play(url, position: playPosition);
    if (result == 1) setState(() => _playerState = PlayerState.playing);

    _audioPlayer.setPlaybackRate(playbackRate: 1.0);


    /* RESTORE position*/
    if (_audioPosition?.getValue() != null) {
      final _res = jsonDecode(_audioPosition.getValue());
      int pos = _audioPosition.getValue() == '{}' ? 0 : _res['audioPosition'];
      _audioPlayer.seek(Duration(milliseconds: pos));
    }

    return result;
  }

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

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

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

Solution

  • In the PreferenceBuilder widget, you should remove the first "Save Position" code, those 2 lines code before the scaffold return overrides the value of the saved audio position with a zero value when the widget builds for the first time, this is because the duration is null when the widget builds for the first time. A better place to save the audio position in sharedpreference is in the

    _audioplayer.onAudioPositionChanged() function in the

    _initAudioPlayer()

    function. You can check the snippet below to get an idea of what I am talking about

         AudioInformation _audio = AudioInformation('${widget.productName}', p?.inMilliseconds?.round() ?? 0);
         _audioPosition.setValue(_audio.toJson().toString());
         setState(() {
           _position = p;
         });
       });
    

    This should solve the issue you are facing