How to find the current song duration in Flutter with audio_service and just_audio

2,078

Once you have your just_audio AudioPlayer set up, you can listen for changes in the duration stream and then make the updates there:

_player.durationStream.listen((duration) {
  final songIndex = _player.playbackEvent.currentIndex;
  print('current index: $songIndex, duration: $duration');
  final modifiedMediaItem = mediaItem.copyWith(duration: duration);
  _queue[songIndex] = modifiedMediaItem;
  AudioServiceBackground.setMediaItem(_queue[songIndex]);
  AudioServiceBackground.setQueue(_queue);
});

Notes:

  • This is inside your audio_service BackgroundAudioTask class.
  • When I tried using _player.currentIndex directly I was getting strange behavior (the first two songs both had index 0 before the index started incrementing) (GitHub issue). That's why I use the playback event here to get the current index.
  • For my example I'm using a List<MediaItem> for the queue. I didn't actually need to use the setQueue in the final line because by UI wasn't listening for changes in the queue, but I suppose it's good to do anyway.

Fuller code example

Here is my whole background_audio_service.dart for reference. It's an adaptation of the documentation example:

import 'dart:async';

import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:just_audio/just_audio.dart';

void audioPlayerTaskEntrypoint() async {
  AudioServiceBackground.run(() => AudioPlayerTask());
}

class AudioPlayerTask extends BackgroundAudioTask {
  AudioPlayer _player = new AudioPlayer();
  AudioProcessingState _skipState;
  StreamSubscription<PlaybackEvent> _eventSubscription;

  List<MediaItem> _queue = [];
  List<MediaItem> get queue => _queue;

  int get index => _player.playbackEvent.currentIndex;
  MediaItem get mediaItem => index == null ? null : queue[index];

  @override
  Future<void> onStart(Map<String, dynamic> params) async {
    _loadMediaItemsIntoQueue(params);
    await _setAudioSession();
    _propogateEventsFromAudioPlayerToAudioServiceClients();
    _performSpecialProcessingForStateTransistions();
    _loadQueue();
  }

  void _loadMediaItemsIntoQueue(Map<String, dynamic> params) {
    _queue.clear();
    final List mediaItems = params['data'];
    for (var item in mediaItems) {
      final mediaItem = MediaItem.fromJson(item);
      _queue.add(mediaItem);
    }
  }

  Future<void> _setAudioSession() async {
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration.music());
  }

  void _propogateEventsFromAudioPlayerToAudioServiceClients() {
    _eventSubscription = _player.playbackEventStream.listen((event) {
      _broadcastState();
    });
  }

  void _performSpecialProcessingForStateTransistions() {
    _player.processingStateStream.listen((state) {
      switch (state) {
        case ProcessingState.completed:
          onStop();
          break;
        case ProcessingState.ready:
          _skipState = null;
          break;
        default:
          break;
      }
    });
  }

  Future<void> _loadQueue() async {
    AudioServiceBackground.setQueue(queue);
    try {
      await _player.load(ConcatenatingAudioSource(
        children:
            queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
      ));
      _player.durationStream.listen((duration) {
        _updateQueueWithCurrentDuration(duration);
      });
      onPlay();
    } catch (e) {
      print('Error: $e');
      onStop();
    }
  }

  void _updateQueueWithCurrentDuration(Duration duration) {
    final songIndex = _player.playbackEvent.currentIndex;
    print('current index: $songIndex, duration: $duration');
    final modifiedMediaItem = mediaItem.copyWith(duration: duration);
    _queue[songIndex] = modifiedMediaItem;
    AudioServiceBackground.setMediaItem(_queue[songIndex]);
    AudioServiceBackground.setQueue(_queue);
  }

  @override
  Future<void> onSkipToQueueItem(String mediaId) async {
    final newIndex = queue.indexWhere((item) => item.id == mediaId);
    if (newIndex == -1) return;
    _skipState = newIndex > index
        ? AudioProcessingState.skippingToNext
        : AudioProcessingState.skippingToPrevious;
    _player.seek(Duration.zero, index: newIndex);
  }

  @override
  Future<void> onPlay() => _player.play();

  @override
  Future<void> onPause() => _player.pause();

  @override
  Future<void> onSeekTo(Duration position) => _player.seek(position);

  @override
  Future<void> onFastForward() => _seekRelative(fastForwardInterval);

  @override
  Future<void> onRewind() => _seekRelative(-rewindInterval);

  @override
  Future<void> onStop() async {
    await _player.dispose();
    _eventSubscription.cancel();
    await _broadcastState();
    await super.onStop();
  }

  /// Jumps away from the current position by [offset].
  Future<void> _seekRelative(Duration offset) async {
    var newPosition = _player.position + offset;
    if (newPosition < Duration.zero) newPosition = Duration.zero;
    if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
    await _player.seek(newPosition);
  }

  /// Broadcasts the current state to all clients.
  Future<void> _broadcastState() async {
    await AudioServiceBackground.setState(
      controls: [
        MediaControl.skipToPrevious,
        if (_player.playing) MediaControl.pause else MediaControl.play,
        MediaControl.skipToNext,
      ],
      androidCompactActions: [0, 1, 2],
      processingState: _getProcessingState(),
      playing: _player.playing,
      position: _player.position,
      bufferedPosition: _player.bufferedPosition,
      speed: _player.speed,
    );
  }

  /// Maps just_audio's processing state into into audio_service's playing
  /// state. If we are in the middle of a skip, we use [_skipState] instead.
  AudioProcessingState _getProcessingState() {
    if (_skipState != null) return _skipState;
    switch (_player.processingState) {
      case ProcessingState.none:
        return AudioProcessingState.stopped;
      case ProcessingState.loading:
        return AudioProcessingState.connecting;
      case ProcessingState.buffering:
        return AudioProcessingState.buffering;
      case ProcessingState.ready:
        return AudioProcessingState.ready;
      case ProcessingState.completed:
        return AudioProcessingState.completed;
      default:
        throw Exception("Invalid state: ${_player.processingState}");
    }
  }
}

Then in my state management class I got a stream from AudioService like this:

Stream<AudioPlayerState> get mediaStateStream =>
    Rx.combineLatest2<Duration, MediaItem, AudioPlayerState>(
        AudioService.positionStream,
        AudioService.currentMediaItemStream,
        (position, mediaItem) => AudioPlayerState(position, mediaItem.duration));

where AudioPlayerState is

class AudioPlayerState {
  const AudioPlayerState(this.currentTime, this.totalTime);
  final Duration currentTime;
  final Duration totalTime;

  const AudioPlayerState.initial() : this(Duration.zero, Duration.zero);
}

And I used a StreamBuilder in the Flutter UI to listen to mediaStateStream and update my audio player seek bar widget.

Share:
2,078
Suragch
Author by

Suragch

Read my story here: Programming was my god

Updated on November 28, 2022

Comments

  • Suragch
    Suragch over 1 year

    The when you set the MediaItem in audio_service you don't know the song duration yet because just_audio hasn't had a change to tell you at this point.

    The FAQs say to update the MediaItem like this:

    modifiedMediaItem = mediaItem.copyWith(duration: duration);
    AudioServiceBackground.setMediaItem(modifiedMediaItem);
    

    But it is unclear to me how or where to do this. The example app in the GitHub repo sidesteps this problem by providing precomputed times. (GitHub issue)

    How and where do I transfer the duration from just_audio to audio_service so that it can update the listeners?

    I found something that works so I'm adding an answer below.

  • dorado
    dorado almost 3 years
    Hi is it possible to provide a github link for this project?
  • Suragch
    Suragch almost 3 years
    @dorado, I don't currently have a public example on GitHub but I'm working on an article about audio_service that will include an example. Follow me on Twitter or Medium to get updates. suragch.dev
  • dorado
    dorado almost 3 years
    That's awesome. Will add you on twitter. Thanks!
  • Suragch
    Suragch over 2 years
  • Asjad Siddiqui
    Asjad Siddiqui about 2 years
    Hi, can you update this code to work with audio_service 0.18 ? I followed the migration guide but my player notifications are black and the duration is not updating correctly
  • Asjad Siddiqui
    Asjad Siddiqui about 2 years
    @Suragch can you please help?
  • Suragch
    Suragch about 2 years
    @AsjadSiddiqui, I'm sorry. I do hope to update my tutorial some day, but I'm not sure when that will happen.