Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConcatenatingAudioSource: currentIndexStream - a way to check if the index change is due to either a track ending or a manual seek #174

Closed
hacker1024 opened this issue Sep 3, 2020 · 11 comments
Assignees
Labels
1 backlog enhancement New feature or request

Comments

@hacker1024
Copy link

Is your feature request related to a problem? Please describe.
The audio server I'm using requires different API calls to skip to the next track and skip to an arbitrary track. The latter is slower than the former, and song recommendations are effected, so I'd like to use the appropriate call when possible.

Describe the solution you'd like
It'd be great if there was perhaps a dedicated stream for index changes that not only includes the index, but also a reason for the change - something like ExoPlayer's onPositionDiscontinuity callback.

Describe alternatives you've considered
A semi-workaround at the moment is to store the last index in a variable and compare that with the new one to see if there's a gap of more than 1 - but this still leaves it impossible to tell the difference between a user skipping to the next song and the song ending.

Additional context
That's it.

@ryanheise
Copy link
Owner

Can you explain a little more about how this API works? If you have to call an API other than just_audio to skip to a track, then I assume you are not using just_audio's ConcatenatingAudioSource to manage skipping.

Then just to clarify, why wouldn't this work?

if (newIndex = index + 1) api.next();
else if (newIndex = index - 1) api.previous();
else api.jumpTo(newIndex);

@hacker1024
Copy link
Author

hacker1024 commented Sep 4, 2020

I'm actually using both - the API allows me to see up to one song in advance, so I add the upcoming song to the ConcatenatingMediaSource for gapless playback.

I need to listen to position changes so I know when to tell the API the track ended, and request the next upcoming song.

Your above example works, but the API has different calls for skipping to the next track, and being notified that the track ended naturally. These can affect song recommendations, so it's important I use the right one.

At the moment, there's no way to known which one to use (other than perhaps flipping a boolean in audio_service's onSkipToNext, but that's pretty messy).

ExoPlayer at least sends a different event enum for both cases, but the plugin does the exact same thing for both - so the information is lost.

@ryanheise
Copy link
Owner

I understand you now. I'll have to think on how to fit this into the current API, but in the meantime, you can possibly try a better workaround:

bool skipped = false;

void skipTo(int index) {
  skipped = true;
  player.seek(Duration.zero, index: index);
}

player.currentIndexStream.listen((index) {
  if (index == null) return; // haven't tested this
  if (skipped) {
    // call API one way
  } else {
    // call API the other way
  }
  skipped = false;
});

@hacker1024
Copy link
Author

Thanks. That's what I'll do for now.

@smkhalsa
Copy link
Contributor

As mentioned in #316, I have a similar requirement that could be resolved with something similar to ExoPlayer's onPositionDiscontinuity listener.

I have a music player that uses ConcatenatingAudioSource for gapless playback. I'd like to log a "listen" every time a user listens to a song for at least 30 seconds. If the song is repeated (such as when LoopMode is set to one) an additional "listen" should be logged.

Simply tracking changes to currentIndex isn't sufficient for this use case (as illustrated in the LoopMode.one comment above).

I tried listening to the position and duration streams and logging when they are equal, but they aren't reliably equal for each listen.

I suppose I could do something like:

bool isComplete = false
if (position > duration - Duration(milliseconds: 100)) {
  isComplete = true;
  /// log the listen
} else if (position == Duration.zero) {
  isComplete = false;
}

But this has some code smell.

@ryanheise
Copy link
Owner

That's probably the easiest way to go about it. Another way is to observe playbackEventStream and look specifically at the updateTime and updatePosition fields. A new playback event is emitted whenever there is a time discontinuity. There are other reasons that cause an event to be emitted so this can't be relied on alone, but the idea of the two fields mentioned above is that they are used to project the "current" position from the position at the last time discontinuity based on when it happened and how much time has elapsed since then. This is the implementation of the position getter:

  /// The current position of the player.
  Duration get position {
    if (playing && processingState == ProcessingState.ready) {
      final result = _playbackEvent.updatePosition +
          (DateTime.now().difference(_playbackEvent.updateTime)) * speed;
      return _playbackEvent.duration == null ||
              result <= _playbackEvent.duration!
          ? result
          : _playbackEvent.duration!;
    } else {
      return _playbackEvent.updatePosition;
    }
  }

So what you could do is compare the current playback event with the last playback event and use the same calculation above to project both playback events to their projected current positions and compare if they're approximately equal. If they're not, then you have detected an actual time discontinuity. My previous workaround listed above will help you then distinguish that from a time discontinuity caused by a manual seek.

(playbackEventStream as ValueStream<Duration>).pairwise().listen((pair) {
  final lastEvent = pair.first;
  final thisEvent = pair.last;
  // positionOf would be your copy of the above projection calculation
  final lastProjectedPosition = positionOf(lastEvent);
  final thisProjectedPosition = positionOf(thisEvent);
  // make your own roughlyEqual function with some desired epsilon
  if (!roughlyEqual(lastProjectedPosition, thisProjectedPosition)) {
    // time discontinuity
    if (manualSeek) {
      // handle manual seek
      manualSeek = false;
    } else {
      // track completed
    }
  }
});

You'd also need to set manualSeek:

bool manualSeek = false;

Future<void> seek(final Duration position, {int index}) async {
  manualSeek = true;
  await _player.seek(position, index: index);
}

@ryanheise
Copy link
Owner

This is now implemented in the feature/position_discontinuity branch.

Usage:

player.positionDiscontinuityReasonStream.listen((reason) {
  switch (reason) {
    case PositionDiscontinuityReason.seek:
      break;
    case PositionDiscontinuityReason.autoAdvance:
      break;
  }
});

The implementation was purely in Dart by observing the cues mentioned in the comments above, so it should work across platforms. Not sure about the API naming, or whether I need other enum values, so feedback is welcome before I release this.

@ryanheise
Copy link
Owner

I've changed the API to this:

player.positionDiscontinuityStream.listen((discontinuity) {
  switch (discontinuity.reason) {
    case PositionDiscontinuityReason.seek:
      break;
    case PositionDiscontinuityReason.autoAdvance:
      break;
  }
});

The stream name is now shorter and refers to a data structure containing 1) the reason, 2) the previous playback event, and 3) the present playback event.

I've also updated example_playlist.dart to demonstrate the API by showing a snackbar message whenever reaching the end of any item in the playlist.

@ryanheise
Copy link
Owner

This is now merged.

@ryanheise
Copy link
Owner

Published! 0.9.28.

@github-actions
Copy link

github-actions bot commented Aug 1, 2022

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 just_audio.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
1 backlog enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants