bluefireteam / audioplayers

A Flutter package to play multiple audio files simultaneously (Android/iOS/web/Linux/Windows/macOS)
https://pub.dartlang.org/packages/audioplayers
MIT License
2.01k stars 844 forks source link

Cannot get iOS Lock Screen control playback slider to work #1138

Open rjrogerto opened 2 years ago

rjrogerto commented 2 years ago

Hi

I cannot get iOS Lock Screen slider to work. When I drag the player slider forward on the iOS Lock Screen ,it causes the audio to jump back and resets to the beginning of the audio track, it does not seek forwards to the correct dragged point.

Can someone please help me to determine what I am doing wrong. Any assistance will be greatly appreciate .

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/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;

  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})
      : super(key: key);

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

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

  late final Function callback;

  late AudioPlayer _audioPlayer;
  PlayerState? _audioPlayerState;
  Duration? _duration;
  Duration? _position;
  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;

  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;

  _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();
    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;
    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: 5.0,
                      thumbColor: SliderColor,
                      thumbShape:
                          RoundSliderThumbShape(enabledThumbRadius: 12.0),
                      overlayColor: SliderColor,
                      overlayShape:
                          RoundSliderOverlayShape(overlayRadius: 28.0),
                    ),
                    child: Slider(
                      onChanged: (v) {
                        final duration = _duration;
                        if (duration == null) {
                          return;
                        }
                        final Position = v * duration.inMilliseconds;
                        _audioPlayer
                            .seek(Duration(milliseconds: Position.round()));
                      },
                      value: (_position != null &&
                              _duration != null &&
                              _position!.inMilliseconds > 0 &&
                              _position!.inMilliseconds <
                                  _duration!.inMilliseconds)
                          ? _position!.inMilliseconds /
                              _duration!.inMilliseconds
                          : 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,
                  ),
                ),
              ],
            ),
          ],
        ),
        // Text('State: $_audioPlayerState'),
      ],
    );
  }

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

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

      if (Theme.of(context).platform == TargetPlatform.iOS) {
        // optional: listen for notification updates in the background
        _audioPlayer.notificationService.startHeadlessService();

        // set at least title to see the notification bar on ios.
        _audioPlayer.notificationService.setNotification(
          title: widget.title,
          artist: widget.name,
          albumTitle: 'Album',
          imageUrl: 'https://static.wixstatic.com/media/c598.png',
          forwardSkipInterval: const Duration(seconds: 30), // default is 30s
          backwardSkipInterval: const Duration(seconds: 30), // default is 30s
          duration: duration,
          enableNextTrackButton: true,
          enablePreviousTrackButton: true,
        );
      }
    });

    _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) {
      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;
    final 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";
}
igorfastronicorrea commented 2 years ago

did you manage to run the url of an mp3 with HTTP?

rjrogerto commented 2 years ago

url with http of an m4a still presents the same issue, it plays fine within the app but on iOS Lock Screen the slider does not work, it has same bug as I described above. The code I use to call this is from another screen is below:

      Navigator.of(context, rootNavigator: true).push(
                              CupertinoPageRoute(
                                  builder: (_) => const PlayerScreen(
                                    url:
                                    'http://my_audio_file.m4a',
                                    title: 'Lying down movement practice',
                                    name: "Meditation",
                                  )),
                            );
q240252859 commented 1 year ago

可以链接耳机进行播放吗?